From 3794cd333fe838637b5e405fa4271ef4a758ea35 Mon Sep 17 00:00:00 2001
From: Bruce <brucef@ssec.wisc.edu>
Date: Wed, 11 Dec 2013 10:31:13 -0600
Subject: [PATCH] Big futz around, version bumped to 0.4a

---
 metobs/data/__init__.py        | 351 +--------------------------------
 metobs/data/calc.py            | 144 ++++++++++++++
 metobs/data/tests/__init__.py  |   0
 metobs/data/tests/test_time.py |  15 ++
 metobs/data/tests/test_wind.py |  31 +++
 metobs/data/time.py            |  16 ++
 metobs/data/units.py           |  54 +++++
 metobs/data/util.py            | 132 +++++++++++++
 setup.cfg                      |   3 -
 setup.py                       |  30 +--
 10 files changed, 402 insertions(+), 374 deletions(-)
 create mode 100644 metobs/data/calc.py
 create mode 100644 metobs/data/tests/__init__.py
 create mode 100644 metobs/data/tests/test_time.py
 create mode 100644 metobs/data/tests/test_wind.py
 create mode 100644 metobs/data/time.py
 create mode 100644 metobs/data/units.py
 create mode 100644 metobs/data/util.py
 delete mode 100644 setup.cfg

diff --git a/metobs/data/__init__.py b/metobs/data/__init__.py
index 401a47c..33f4c4a 100644
--- a/metobs/data/__init__.py
+++ b/metobs/data/__init__.py
@@ -1,349 +1,4 @@
-"""Functions for calculating the dervied data.
 
-All linear conversion support ``numpy.ndarray`` objects.
-"""
-__version__ = '$Revision: 1.15 $'
-__docformat__ = 'restructuredtext en'
-
-import math
-from datetime import timedelta
-
-import fpconst
-from numpy import array, average, zeros, radians, degrees, arctan2, sin, cos, ndarray, log, minimum, exp, sqrt
-from numpy.ma import masked_array, average as masked_average, MaskedArray, masked_where, column_stack
-try:
-    from pydap.client import open_url as dapopen
-except:
-    dapopen = open 
-
-from metobs import mytime
-
-#: Not a number representation
-NaN = fpconst.NaN
-
-#: function to determine if a number is NaN
-is_nan = fpconst.isNaN
-
-def average_for_interval(basetime, arr, interval):
-    """Generate averages for timestamped data at a particular interval. Averages
-    may be produced from different numbers of points depending on the number of
-    points in each interval bucket. Returns data averaged at the specified 
-    interval. Times are averaged as well as the data meaning the time will 
-    represent the middle of the average.
-    
-    :param basetime: Time to use as the base for computing intervals
-    :type basetime: ``datetime.datetime``
-    :param arr: array of data where index 0 in each row is an epoch time in seconds.
-    :type arr: ``numpy.array``
-    :param interval: the averaging interval in seconds
-    """
-    if type(arr) == ndarray:
-        avg_func = average
-        array_func = array
-    elif type(arr) == MaskedArray:
-        avg_func = masked_average
-        array_func = masked_array
-    arr_out = []
-    stop_dt = basetime + timedelta(seconds=interval)
-    stop_idx = 1
-    start_idx = 0
-    tz_cls = stop_dt.tzinfo and stop_dt.tzinfo.__class__
-    
-    # advance through data until there is a timestamp in the averaging interval
-    while mytime.seconds_to_datetime(arr[0][0], tz=tz_cls) >= stop_dt:
-        stop_dt += timedelta(seconds=interval)
-    
-    for row_idx in range(arr.shape[0]):
-        row = arr[row_idx]
-        cur_dt = mytime.seconds_to_datetime(row[0], tz=tz_cls)
-        
-        # determine stop index
-        if cur_dt < stop_dt:
-            stop_idx +=1
-        # stop and average
-        else:
-            s = mytime.seconds_to_datetime(arr[start_idx][0], tz=tz_cls)
-            # matrix of data to be averaged
-            avg_input = arr[start_idx:stop_idx,1:]
-            avg_out = array(zeros(arr[0].shape[0]))
-            if type(arr) == ndarray: avg_out.fill(NaN)
-            elif type(arr) == MaskedArray: masked_where(avg_out == 0, avg_out)
-            
-            # timestamp for center of averaging interval
-            int_dt = stop_dt - timedelta(seconds=(interval/2))
-            avg_out[0] = mytime.datetime_to_epoch(int_dt)
-            avg_out[1:] = avg_func(avg_input, axis=0)
-            arr_out.append(avg_out)
-            
-            # adjust conditions for next average
-            start_idx = row_idx
-            stop_idx = start_idx
-            stop_dt += timedelta(seconds=interval)
-
-    avg_arr = arr[start_idx:,:]
-    avg_arr = avg_func(avg_arr, axis=0)
-    arr_out.append(avg_arr)
-    if type(arr) == ndarray: arr_out = array_func(arr_out)
-    elif type(arr) == MaskedArray:
-        arr_out = column_stack(arr_out).transpose()
-    return arr_out
-
-def average_for_interval_degree(basetime, arr, interval):
-    """Generate averages for timestamped data at a particular interval.
-    Data must be in degrees.  See documentation for average_for_interval()
-    for more information on the averaging of this function.
-
-    :param basetime: Time to use as the base for computing intervals
-    :type basetime: ``datetime.datetime``
-    :param arr: array of data in degrees where index 0 in each row is an epoch time in seconds.
-    :type arr: ``numpy.array``
-    :param interval: the averaging interval in seconds
-    """
-
-    # Create 2 x and y component arrays from radians
-    y_comp = array((arr[:,0],sin(radians(arr[:,1])))).transpose()
-    x_comp = array((arr[:,0],cos(radians(arr[:,1])))).transpose()
-
-    # Average the 2 components
-    y_comp = average_for_interval(basetime, y_comp, interval)
-    x_comp = average_for_interval(basetime, x_comp, interval)
-
-    # Convert back to degrees and then add 360 to negative degrees
-    new_degree = array((y_comp[:,0],degrees(arctan2(y_comp[:,1], x_comp[:,1]))))
-    for i in range(new_degree[1].size):
-        if new_degree[1,i] < 0:
-            new_degree[1,i] += 360
-    new_degree = new_degree.transpose()
-
-    return new_degree
-
-def get_nc_array(url, vars):
-    """Get an array of data for a date and set of vars.
-
-    :param url: where to retreive data from
-    :param vars: netcdf variable names
-    :type vars: ``list``
-
-    :raise ValueError: If variable name is not in file
-    :raise IndexError: If variable has no data
-    """
-    nc = dapopen(url)
-    arr = []
-    for nm in vars:
-        if nm not in nc.keys():
-            raise ValueError("'%s' not a variable in %s" %(nm, nc.name))
-        if nc[nm].shape[0] == 0:
-            raise IndexError("No data for '%s'" % nm)
-        arr.append(nc[nm][:])
-        
-    return array(arr).transpose()
-
-def approx_eq(this, that, delta=.000001):
-    """Check if to decimal numbers are approximately equal.
-    
-    :param this: operand one 
-    :param that: operand two
-    :param delta: amount of allowable difference
-    """
-    if this > that - delta and this < that + delta:
-        return True
-    return False
-
-def dewpoint(tempC, relhum):
-    """
-    Algorithm from Tom Whittaker tempC is the temperature in degrees Celsius,
-    relhum is the relative humidity as a percentage.
-    
-    :param tempC: temperature in celsius
-    :param relhum: relative humidity as a percentage
-    """
-    if tempC is None or relhum is None:
-        return NaN
-    
-    gasconst = 461.5
-    latheat = 2500800.0
-
-    dp = 1.0 / ( 1.0 / ( 273.15 + tempC ) - gasconst * log(  (0.0 + relhum) / 100 ) / \
-         ( latheat - tempC * 2397.5 ))
-
-    return minimum(dp - 273.15, tempC)
-
-def relhum(airTempK, dewpointTempK):
-    """
-    Algorithm derived by David Hoese from the above
-    dewpoint(tempC, relhum) function, both parameters are in Kelvin units.
-
-    :param airTempK: air temperature in Kelvin
-    :param dewpointTempK: dewpoint temp in Kelvin
-    """
-    if airTempK == None or dewpointTempK == None:
-        return NaN
-
-    gas_constant = 461.5
-    latheat = 2500800.0
-
-    # Only one section of the equation
-    latpart = (latheat-(airTempK-273.15)*2397.5)
-    relativehum = 100 * math.e**((latpart/airTempK - latpart/dewpointTempK)/gas_constant)
-
-    return relativehum
-
-def potentialtemp(airTempK, pressureMB):
-    """
-    Algorithm from David Hoese to calculate potential temperature.
-
-    :param airTempK: air temperature in Kelvin
-    :param pressureMB: air pressure in millibars
-    """
-    if airTempK == None or  pressureMB == None:
-        return NaN
-
-    pT = airTempK * (pressureMB.max()/pressureMB)**.286
-
-    return pT
-
-def linearConvert(data, factor=1, offset=0):
-    """
-    >>> linearConvert(0)
-    0.0
-    >>> linearConvert(1)
-    1.0
-    >>> linearConvert(1, 1, 0)
-    1.0
-    >>> linearConvert(1, 90, 10)
-    100.0
-    """
-    return data * float(factor) + offset
-
-def mm2in(val):
-    """Convert millimeters to inches.
-    """
-    return linearConvert(val, 0.0393700787)
-
-def in2mm(val):
-    """Convert inches to millimeters.
-    """
-    return linearConvert(val, 1/0.0393700787)
-
-def c2f (val):
-    """Degrees celsius to fahrenheit.
-    
-    >>> c2f(0)
-    32.0
-    >>> c2f(100)
-    212.0
-    """
-    return linearConvert(val, (9/5.), 32)
-
-def f2c(val):
-    """Degrees fahrenheit to celsius.
-
-    >>> f2c(32)
-    0.0
-    >>> f2c(212)
-    100.0
-    """
-    return linearConvert(val-32, (5/9.), 0)
-
-def mps2mph (val):
-    """Speed in meters per second to miles per hour.
-    """
-    return linearConvert(val, 2.23694)
-
-def mps2knots (val):
-    """Meters per second to knots.
-    """
-    return linearConvert(val, 1.9438445)
-
-def knots2mps(val):
-    """Speed in knots to meters per second.
-    """
-    return linearConvert(val, 1/1.9438445)
-
-def altimeter(p, alt):
-    """Compute altimeter from pressure and altitude.
-    
-    Converted from code provided by TomW.
-    
-    :param p: pressure in hPa.
-    :param alt: altitude of the measurement in meters.
-    
-    :returns: altimeter in inHg
-    """
-    n = .190284
-    c1= .0065 * pow(1013.25, n)/288.
-    c2 = alt / pow( (p-.3), n)
-    ff = pow( 1.+c1*c2, 1./n)
-    return ((p-.3) * ff * 29.92 / 1013.25)
-
-
-def dir2txt (val):
-    """Convert degrees [0, 360) to a textual representation.
-    
-    :param val: decimal degrees
-    
-    >>> dir2txt(0)
-    'N'
-    >>> dir2txt(90)
-    'E'
-    >>> dir2txt(180)
-    'S'
-    >>> dir2txt(270)
-    'W'
-    >>> dir2txt(359)
-    'N'
-    """
-    assert val >= 0 and val < 360, "'%s' out of range" % val
-    dirs = ("NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW")
-    
-    if ((val >= 348.75 and val <= 360) or val >= 0 and val < 11.25): return "N"
-    
-    # 1/2 degree increment between the directions
-    i = 11.25; 
-    for dir in dirs:
-        if val >= i and val < (i + 22.5):
-            return dir
-        i += 22.5
-
-def array_convert(arr, sunits, cunits):
-    """Converts the data in array arr from sunits to cunits.
-
-    :param arr: Array to be converted
-    :type arr: ndarray
-    :param sunits: Starting Units = 'C', 'F', 'm/s', 'knots', 'hpa', 'deg', 'in', 'mm', 'w/m2'
-    :type sunits: string
-    :param cunits: Convert to Units = Same choices as sunits
-    :type cunits: string
-    """
-    if sunits == cunits: return arr
-    if not isinstance(arr, ndarray): raise ValueError("Array must be of type numpy.ndarray, not %r" % (type(arr),))
-    if sunits == 'c':
-        if cunits == 'f':
-            a = c2f(arr)
-            return a
-    elif sunits == 'f':
-        if cunits == 'c':
-            a = f2c(arr)
-            return a
-    elif sunits == 'm/s':
-        if cunits == 'knots':
-            a = mps2knots(arr)
-            return a
-    elif sunits == 'knots':
-        if cunits == 'm/s':
-            a = knots2mps(arr)
-            return a
-    elif sunits == 'in':
-        if cunits == 'mm':
-            a = in2mm(arr)
-            return a
-    elif sunits == 'mm':
-        if cunits == 'in':
-            a = mm2in(arr)
-            return a
-
-    raise ValueError("sunits or cunits was not an acceptable unit")
-
-if __name__ == '__main__':
-    import doctest
-    doctest.testmod()
+from .time import to_unix_timestamp
+from .units import *
+from .calc import *
diff --git a/metobs/data/calc.py b/metobs/data/calc.py
new file mode 100644
index 0000000..322c037
--- /dev/null
+++ b/metobs/data/calc.py
@@ -0,0 +1,144 @@
+import math
+
+import numpy as np
+
+NaN = float('nan')
+is_nan = lambda a: a != a
+
+
+def dewpoint(tempC, relhum):
+    """
+    Algorithm from Tom Whittaker tempC is the temperature in degrees Celsius,
+    relhum is the relative humidity as a percentage.
+
+    :param tempC: temperature in celsius
+    :param relhum: relative humidity as a percentage
+    """
+    if tempC is None or relhum is None:
+        return NaN
+
+    gasconst = 461.5
+    latheat = 2500800.0
+
+    dp = 1.0 / (1.0 / (273.15 + tempC) - gasconst * np.log((0.0 + relhum) / 100) /
+                (latheat - tempC * 2397.5))
+
+    return min(dp - 273.15, tempC)
+
+
+def relhum(airTempK, dewpointTempK):
+    """
+    Algorithm derived by David Hoese from the above
+    dewpoint(tempC, relhum) function, both parameters are in Kelvin units.
+
+    :param airTempK: air temperature in Kelvin
+    :param dewpointTempK: dewpoint temp in Kelvin
+    """
+    if airTempK == None or dewpointTempK == None:
+        return NaN
+
+    gas_constant = 461.5
+    latheat = 2500800.0
+
+    # Only one section of the equation
+    latpart = (latheat - (airTempK - 273.15) * 2397.5)
+    relativehum = 100 * math.e ** ((latpart / airTempK - latpart / dewpointTempK) / gas_constant)
+
+    return relativehum
+
+
+def potentialtemp(airTempK, pressureMB):
+    """
+    Algorithm from David Hoese to calculate potential temperature.
+
+    :param airTempK: air temperature in Kelvin
+    :param pressureMB: air pressure in millibars
+    """
+    if airTempK == None or pressureMB == None:
+        return NaN
+
+    pT = airTempK * (pressureMB.max() / pressureMB) ** .286
+
+    return pT
+
+
+def altimeter(p, alt):
+    """Compute altimeter from pressure and altitude.
+
+    Converted from code provided by TomW.
+
+    :param p: pressure in hPa.
+    :param alt: altitude of the measurement in meters.
+
+    :returns: altimeter in inHg
+    """
+    n = .190284
+    c1 = .0065 * pow(1013.25, n) / 288.
+    c2 = alt / pow((p - .3), n)
+    ff = pow(1. + c1 * c2, 1. / n)
+    return ((p - .3) * ff * 29.92 / 1013.25)
+
+
+def dir2txt(val):
+    """Convert degrees [0, 360) to a textual representation.
+
+    :param val: decimal degrees
+
+    >>> dir2txt(0)
+    'N'
+    >>> dir2txt(90)
+    'E'
+    >>> dir2txt(180)
+    'S'
+    >>> dir2txt(270)
+    'W'
+    >>> dir2txt(359)
+    'N'
+    """
+    assert val >= 0 and val < 360, "'%s' out of range" % val
+    dirs = ("NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW")
+
+    if ((val >= 348.75 and val <= 360) or val >= 0 and val < 11.25): return "N"
+
+    # 1/2 degree increment between the directions
+    i = 11.25;
+    for dir in dirs:
+        if val >= i and val < (i + 22.5):
+            return dir
+        i += 22.5
+
+
+def wind_vector_components(windspd, winddir):
+    """Decompose scalar or list/array polar wind direction and speed data
+    into the horizontal and vertical vector components and speed vector.
+
+    Inputs can be scalar or arrays.
+    """
+    dir_rad = np.deg2rad(winddir)
+    spd_arr = np.array(windspd)
+    V_e = spd_arr * np.sin(dir_rad)
+    V_n = spd_arr * np.cos(dir_rad)
+    U_spd = np.sqrt(pow(V_e, 2) + pow(V_n, 2))
+    return V_e, V_n, U_spd
+
+
+def wind_vector_degrees(vector_east, vector_north):
+    """Re-compose horizontal (east/west) and vertical (north/south) vector
+    components into wind direction in degrees.
+
+    Inputs can be scalar or arrays.
+    """
+    rads = np.arctan2(vector_east, vector_north)
+    winddir = np.rad2deg(rads)
+    if isinstance(winddir, np.ndarray):
+        winddir[np.less(winddir, 0)] += 360
+    elif winddir < 0:
+        winddir += 360
+    return winddir % 360
+
+
+def mean_wind_vector(windspd, winddir):
+    V_e, V_n, V_spd = wind_vector_components(windspd, winddir)
+    avg_dir = wind_vector_degrees(np.mean(V_e), np.mean(V_n))
+
+    return avg_dir, np.mean(V_spd)
diff --git a/metobs/data/tests/__init__.py b/metobs/data/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/metobs/data/tests/test_time.py b/metobs/data/tests/test_time.py
new file mode 100644
index 0000000..cee15d7
--- /dev/null
+++ b/metobs/data/tests/test_time.py
@@ -0,0 +1,15 @@
+from datetime import datetime
+
+
+def test_to_unix_timestamp():
+    from metobs.data.time import to_unix_timestamp
+    assert to_unix_timestamp(datetime(1970, 1, 1)) == 0
+
+
+def test_hhmm_to_secs():
+    from metobs.data.time import hhmm_to_offset
+
+    assert hhmm_to_offset('2400') == 86400, "Can't handle > 23:59"
+    assert hhmm_to_offset('2401') == 86460, "Can't handle > 23:59"
+    assert hhmm_to_offset('0') == 0, "Can't handle short times"
+    assert hhmm_to_offset('001') == 60, "Can't handle leading 0"
diff --git a/metobs/data/tests/test_wind.py b/metobs/data/tests/test_wind.py
new file mode 100644
index 0000000..bd8a9fe
--- /dev/null
+++ b/metobs/data/tests/test_wind.py
@@ -0,0 +1,31 @@
+import unittest
+
+
+class MeanWindVectorTests(unittest.TestCase):
+
+    def _fut(self, winddir, windspd=None):
+        from metobs.data.calc import mean_wind_vector
+        windspd = windspd or [1]*len(winddir)
+        return mean_wind_vector(windspd, winddir)[0]
+
+    def test_spanning_0_degrees(self):
+        winddir = self._fut([315, 45])
+        self.assertAlmostEqual(winddir, 0)
+
+    def test_spanning_cardinal_directions(self):
+        self.assertAlmostEqual(self._fut([45, 135]), 90)
+        self.assertAlmostEqual(self._fut([135, 225]), 180)
+        self.assertAlmostEqual(self._fut([225, 315]), 270)
+        self.assertAlmostEqual(self._fut([315, 45]), 0)
+
+    def test_all_zeros(self):
+        self.assertAlmostEqual(self._fut([0, 0]), 0)
+
+    def test_zero_windspd(self):
+        self.assertAlmostEqual(self._fut([0, 0], windspd=[0, 0]), 0)
+
+    def test_45s(self):
+        self.assertAlmostEqual(self._fut([0, 90]), 45)
+        self.assertAlmostEqual(self._fut([90, 180]), 135)
+        self.assertAlmostEqual(self._fut([180, 270]), 225)
+        self.assertAlmostEqual(self._fut([270, 0]), 315)
diff --git a/metobs/data/time.py b/metobs/data/time.py
new file mode 100644
index 0000000..a594113
--- /dev/null
+++ b/metobs/data/time.py
@@ -0,0 +1,16 @@
+from datetime import timedelta
+from calendar import timegm
+
+
+def to_unix_timestamp(dtval):
+    """Convert a datetime to a unix timestamp.
+    """
+    return timegm(dtval.utctimetuple())
+
+def hhmm_to_offset(hhmm):
+    """Convert a string time, possibly with missing hours and minutes, to an
+    offset of seconds.
+    """
+    hhmm = '{:04d}'.format(int(hhmm))
+    return timedelta(hours=int(hhmm[0:2]),
+                     minutes=int(hhmm[2:])).total_seconds()
diff --git a/metobs/data/units.py b/metobs/data/units.py
new file mode 100644
index 0000000..6583b6a
--- /dev/null
+++ b/metobs/data/units.py
@@ -0,0 +1,54 @@
+
+
+from .util import linear_convert
+
+def mm2in(val):
+    """Convert millimeters to inches.
+    """
+    return linear_convert(val, 0.0393700787)
+
+
+def in2mm(val):
+    """Convert inches to millimeters.
+    """
+    return linear_convert(val, 1 / 0.0393700787)
+
+
+def c2f(val):
+    """Degrees celsius to fahrenheit.
+
+    >>> c2f(0)
+    32.0
+    >>> c2f(100)
+    212.0
+    """
+    return linear_convert(val, (9 / 5.), 32)
+
+
+def f2c(val):
+    """Degrees fahrenheit to celsius.
+
+    >>> f2c(32)
+    0.0
+    >>> f2c(212)
+    100.0
+    """
+    return linear_convert(val - 32, (5 / 9.), 0)
+
+
+def mps2mph(val):
+    """Speed in meters per second to miles per hour.
+    """
+    return linear_convert(val, 2.23694)
+
+
+def mps2knots(val):
+    """Meters per second to knots.
+    """
+    return linear_convert(val, 1.9438445)
+
+
+def knots2mps(val):
+    """Speed in knots to meters per second.
+    """
+    return linear_convert(val, 1 / 1.9438445)
\ No newline at end of file
diff --git a/metobs/data/util.py b/metobs/data/util.py
new file mode 100644
index 0000000..d36aa09
--- /dev/null
+++ b/metobs/data/util.py
@@ -0,0 +1,132 @@
+from datetime import datetime, timedelta
+
+from .units import *
+from .time import to_unix_timestamp
+
+import numpy as np
+from numpy.ma import (masked_array, average as masked_average, MaskedArray,
+                      masked_where, column_stack)
+
+NaN = float('nan')
+
+
+def linear_convert(data, factor=1, offset=0):
+    """
+    >>> linear_convert(0)
+    0.0
+    >>> linear_convert(1)
+    1.0
+    >>> linear_convert(1, 1, 0)
+    1.0
+    >>> linear_convert(1, 90, 10)
+    100.0
+    """
+    return data * float(factor) + offset
+
+
+def array_convert(arr, sunits, cunits):
+    """Converts the data in array arr from sunits to cunits.
+
+    :param arr: Array to be converted
+    :type arr: ndarray
+    :param sunits: Starting Units = 'C', 'F', 'm/s', 'knots', 'hpa', 'deg', 'in', 'mm', 'w/m2'
+    :param cunits: Convert to Units = Same choices as sunits
+    """
+    if sunits == cunits: return arr
+    if not isinstance(arr, np.ndarray):
+        raise ValueError("Array must be of type numpy.ndarray, not %r" % (type(arr),))
+    if sunits == 'c':
+        if cunits == 'f':
+            a = c2f(arr)
+            return a
+    elif sunits == 'f':
+        if cunits == 'c':
+            a = f2c(arr)
+            return a
+    elif sunits == 'm/s':
+        if cunits == 'knots':
+            a = mps2knots(arr)
+            return a
+    elif sunits == 'knots':
+        if cunits == 'm/s':
+            a = knots2mps(arr)
+            return a
+    elif sunits == 'in':
+        if cunits == 'mm':
+            a = in2mm(arr)
+            return a
+    elif sunits == 'mm':
+        if cunits == 'in':
+            a = mm2in(arr)
+            return a
+
+    raise ValueError("sunits or cunits was not an acceptable unit")
+
+
+def average_for_interval(basetime, arr, interval):
+    """Generate averages for timestamped data at a particular interval. Averages
+    may be produced from different numbers of points depending on the number of
+    points in each interval bucket. Returns data averaged at the specified
+    interval. Times are averaged as well as the data meaning the time will
+    represent the middle of the average.
+
+    :param basetime: Time to use as the base for computing intervals
+    :type basetime: ``datetime.datetime``
+    :param arr: array of data where index 0 in each row is an epoch time in seconds.
+    :type arr: ``numpy.array``
+    :param interval: the averaging interval in seconds
+    """
+    if isinstance(arr, np.ndarray):
+        avg_func = np.average
+        array_func = np.array
+    elif isinstance(arr, MaskedArray):
+        avg_func = masked_average
+        array_func = masked_array
+    else:
+        raise ValueError()
+    arr_out = []
+    stop_dt = basetime + timedelta(seconds=interval)
+    stop_idx = 1
+    start_idx = 0
+
+    # advance through data until there is a timestamp in the averaging interval
+    while datetime.utcfromtimestamp(arr[0][0]) >= stop_dt:
+        stop_dt += timedelta(seconds=interval)
+
+    for row_idx in range(arr.shape[0]):
+        row = arr[row_idx]
+        cur_dt = datetime.utcfromtimestamp(row[0])
+
+        # determine stop index
+        if cur_dt < stop_dt:
+            stop_idx += 1
+        # stop and average
+        else:
+            # matrix of data to be averaged
+            avg_input = arr[start_idx:stop_idx, 1:]
+            avg_out = np.array(np.zeros(arr[0].shape[0]))
+            if isinstance(arr, np.ndarray):
+                avg_out.fill(NaN)
+            elif isinstance(arr, MaskedArray):
+                masked_where(avg_out == 0, avg_out)
+
+            # timestamp for center of averaging interval
+            int_dt = stop_dt - timedelta(seconds=(interval / 2))
+            avg_out[0] = to_unix_timestamp(int_dt)
+            avg_out[1:] = avg_func(avg_input, axis=0)
+            arr_out.append(avg_out)
+
+            # adjust conditions for next average
+            start_idx = row_idx
+            stop_idx = start_idx
+            stop_dt += timedelta(seconds=interval)
+
+    avg_arr = arr[start_idx:, :]
+    avg_arr = avg_func(avg_arr, axis=0)
+    arr_out.append(avg_arr)
+    if isinstance(arr, np.ndarray):
+        arr_out = array_func(arr_out)
+    elif type(arr) == MaskedArray:
+        arr_out = column_stack(arr_out).transpose()
+    return arr_out
+
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index fb3387d..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,3 +0,0 @@
-[egg_info]
-tag_build = b
-tag_svn_revision = true
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 4a58d53..2f2c53c 100644
--- a/setup.py
+++ b/setup.py
@@ -1,32 +1,16 @@
-# If true, then the svn revision won't be used to calculate the
-# revision (set to True for real releases)
-RELEASE = False
 
 from setuptools import setup, find_packages
-import sys, os
-from datetime import datetime
-from glob import glob
-
-classifiers = ""
-version = '0.3'
-
-filelist = [x for x in glob('metobs/**/*') if 'CVS' not in x]
-
-_requires = ['metobs.mytime', 'fpconst>=0.7.2']
 
 setup(name='metobs.data',
-      version=version,
+      version='0.4a',
       description="",
-      long_description="""MetObs data processing libraries.""",
-      classifiers=filter(None, classifiers.split("\n")),
-      keywords='ssec metobs',
-      author='Bruce Flynn, SSEC',
-      author_email='brucef@ssec.wisc.edu',
-      url='',
-      packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
+      url="",
+      packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]),
       namespace_packages=['metobs'],
       include_package_data=True,
       zip_safe=False,
-      install_requires=_requires,
-    dependency_links = ['http://larch.ssec.wisc.edu/cgi-bin/repos.cgi']
+      install_requires=[
+          'numpy'
+      ],
+      dependency_links=['http://larch.ssec.wisc.edu/cgi-bin/repos.cgi']
 )
-- 
GitLab