diff --git a/.travis.yml b/.travis.yml index a5f0488..54bdbae 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,17 +1,14 @@ -language: python +language: minimal sudo: false matrix: fast_finish: true include: - - python: 3.6 - env: TEST_TARGET=default - - python: 3.6 - env: TEST_TARGET=coding_standards - allow_failures: - - python: 3.6 - env: TEST_TARGET=coding_standards + - name: default + env: TEST_TARGET=default PY=3.7 + - python: coding_standards + env: TEST_TARGET=coding_standards PY=3.7 before_install: - wget http://bit.ly/miniconda -O miniconda.sh @@ -19,8 +16,9 @@ before_install: - export PATH="$HOME/miniconda/bin:$PATH" - conda config --set always_yes yes --set changeps1 no --set show_channel_urls true - conda update conda + - conda config --remove channels defaults --force - conda config --add channels conda-forge --force - - conda create --name TEST python=$TRAVIS_PYTHON_VERSION --file requirements.txt --file requirements-dev.txt + - conda create --name TEST python=$PY --file requirements.txt --file requirements-dev.txt - source activate TEST # GUI tests. - "export DISPLAY=:99.0" @@ -31,10 +29,10 @@ install: - python setup.py sdist && version=$(python setup.py --version) && pushd dist && pip install oceans-${version}.tar.gz && popd script: - - if [[ $TEST_TARGET == 'default' ]]; then - pytest -vv oceans --doctest-modules ; - fi - - - if [[ $TEST_TARGET == 'coding_standards' ]]; then - flake8 --max-line-length=105 oceans --exclude=_version.py ; - fi + - if [[ $TEST_TARGET == 'default' ]]; then + pytest -vv oceans --doctest-modules ; + fi + + - if [[ $TEST_TARGET == 'coding_standards' ]]; then + flake8 --max-line-length=105 oceans --exclude=_version.py ; + fi diff --git a/CHANGES.txt b/CHANGES.txt index b0e9fb6..f1f252a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,7 +1,21 @@ Changelog --------- -Version 0.5.0, unreleased +Version 0.6.0 + +* Re-factored datasets and fixed data sources URLs. + +Version 0.5.1 + +* Fix find packages in setup.py #57. + +Version 0.5.0 + +* Fixed pandas rolling_mean deprecation #54. +* Fixed failing tests. +* Added NaN support for MLD #53 and #56. + +Version 0.4.1 * Replaced custom smooth filter for `scipy`'s `ndimage` filter. * Re-factor `datasets` to avoid the `basemap` syntax in favor of a `bbox` like syntax. @@ -12,18 +26,19 @@ Version 0.5.0, unreleased * Removed `shapely` and use `matplotlib.path.Path` in `in_polygon` instead. * Many speed improvements via lazy imports and updates. * Re-added a re-factored version of the filters module. +* Re-factored datasets and fixed many data sources. -Version 0.4.0, 27-Oct-2016. +Version 0.4.0 -* re-written `get_isobath` function that uses latest matplotlib contour machinery. +* Re-written `get_isobath` function that uses latest matplotlib contour machinery. * Use integer indexes to fix `numpy` deprecations. * Change license from MIT to BSD 3-Clause. -Version 0.3.0, 17-Aug-2016. +Version 0.3.0 * Fix `nanmean` and `nanstd` removed from latest `scipy`. -Version 0.2.5, 30-Jul-2015. +Version 0.2.5 * Lazy imports. * Several minor bug fixes. @@ -34,11 +49,11 @@ Version 0.2.4, 05-May-2015. * Re-write of `woa_subset` to use iris instead of Pandas. * Deprecate 'state' option in soundspeed. -Version 0.2.3, 23-Jan-2015. +Version 0.2.3 * Several small bugs and typos fixes. -Version 0.2.2, 18-Aug-2014. +Version 0.2.2 * Mixed Layer Depth functions. * Critical depth and Light extinction coefficient. @@ -48,16 +63,16 @@ Version 0.2.1, 07-Apr-2014. * Python3 support. -Version 0.2.0, 06-Aug-2013. +Version 0.2.0 * Moved CTDProfile to a separated module (python-ctd). -Version 0.1.0, 26-Jun-2012 +Version 0.1.0 * Added several of new functions and sub-modules * CTDProfile class (pandas DataFrame for CTDs). * time_series methods to extend pandas Series. -Version 0.0.1, 13-Oct-2011 +Version 0.0.1 -* Initial release. +* Initial release. \ No newline at end of file diff --git a/oceans/datasets/datasets.py b/oceans/datasets/datasets.py index cec4c5b..aea59a5 100644 --- a/oceans/datasets/datasets.py +++ b/oceans/datasets/datasets.py @@ -8,151 +8,120 @@ from ..ocfis import get_profile, wrap_lon180 -def woa_subset(bbox=[2.5, 357.5, -87.5, 87.5], variable='temperature', clim_type='00', resolution='1.00', full=False): # noqa - """ - Return an iris.cube instance from a World Ocean Atlas 2013 variable at a - given lon, lat bounding box. - - Parameters - ---------- - bbox: list, tuple - minx, maxx, miny, maxy positions to extract. - Choose data `variable` from: - `dissolved_oxygen`, `salinity`, `temperature`, `oxygen_saturation`, - `apparent_oxygen_utilization`, `phosphate`, `silicate`, or `nitrate`. - Choose `clim_type` averages from: - 01-12 :: monthly - 13-16 :: seasonal (North Hemisphere Winter, Spring, Summer, - and Autumn respectively) - 00 :: annual - Choose `resolution` from: - 1 (1 degree), or 4 (0.25 degrees) +def _woa_variable(variable): + _VAR = { + 'temperature': 't', + 'salinity': 's', + 'silicate': 'i', + 'phosphate': 'p', + 'nitrate': 'n', + 'oxygen_saturation': 'O', + 'dissolved_oxygen': 'o', + 'apparent_oxygen_utilization': 'A', + } + v = _VAR.get(variable) + if not v: + raise ValueError( + f'Unrecognizable variable. Expected one of {list(_VAR.keys())}, got "{variable}".' + ) + return v - Returns - ------- - Iris.cube instance with the climatology. - Examples - -------- - >>> import iris - >>> import cartopy.crs as ccrs - >>> import matplotlib.pyplot as plt - >>> import cartopy.feature as cfeature - >>> from cartopy.mpl.gridliner import (LONGITUDE_FORMATTER, - ... LATITUDE_FORMATTER) - >>> LAND = cfeature.NaturalEarthFeature('physical', 'land', '50m', - ... edgecolor='face', - ... facecolor=cfeature.COLORS['land']) - >>> def make_map(bbox, projection=ccrs.PlateCarree()): - ... fig, ax = plt.subplots(figsize=(8, 6), - ... subplot_kw={'projection': projection}) - ... ax.set_extent(bbox) - ... ax.add_feature(LAND, facecolor='0.75') - ... ax.coastlines(resolution='50m') - ... gl = ax.gridlines(draw_labels=True) - ... gl.xlabels_top = gl.ylabels_right = False - ... gl.xformatter = LONGITUDE_FORMATTER - ... gl.yformatter = LATITUDE_FORMATTER - ... return fig, ax - >>> # Extract a 2D surface -- Annual temperature climatology: - >>> import matplotlib.pyplot as plt - >>> from oceans.ocfis import wrap_lon180 - >>> from oceans.colormaps import cm, get_color - >>> import iris.plot as iplt - >>> from oceans.datasets import woa_subset - >>> bbox = [2.5, 357.5, -87.5, 87.5] - >>> kw = {'bbox': bbox, 'variable': 'temperature', 'clim_type': '00', - ... 'resolution': '0.25'} - >>> cube = woa_subset(**kw) - >>> c = cube[0, 0, ...] # Slice singleton time and first level. - >>> cs = iplt.pcolormesh(c, cmap=cm.avhrr) - >>> cbar = plt.colorbar(cs) - >>> # Extract a square around the Mariana Trench averaging into a profile. - >>> bbox = [-143, -141, 10, 12] - >>> kw = {'bbox': bbox, 'variable': 'temperature', 'resolution': '0.25', - ... 'clim_type': None} - >>> fig, ax = plt.subplots(figsize=(5, 5)) - >>> colors = get_color(12) - >>> months = 'Jan Feb Apr Mar May Jun Jul Aug Sep Oct Nov Dec'.split() - >>> months = dict(zip(months, range(12))) - >>> for month, clim_type in months.items(): - ... clim_type = '{0:02d}'.format(clim_type+1) - ... kw.update(clim_type=clim_type) - ... cube = woa_subset(**kw) - ... grid_areas = iris.analysis.cartography.area_weights(cube) - ... c = cube.collapsed(['longitude', 'latitude'], iris.analysis.MEAN, - ... weights=grid_areas) - ... z = c.coord(axis='Z').points - ... l = ax.plot(c[0, :].data, z, label=month, color=next(colors)) - >>> ax.grid(True) - >>> ax.invert_yaxis() - >>> leg = ax.legend(loc='lower left') - >>> _ = ax.set_ylim(200, 0) +def _woa_url(variable, time_period, resolution): + base = 'https://data.nodc.noaa.gov/thredds/dodsC' - """ - import iris + v = _woa_variable(variable) if variable not in ['salinity', 'temperature']: - resolution = '1.00' - decav = 'all' - msg = '{} is only available at 1 degree resolution'.format - warnings.warn(msg(variable)) + pref = 'woa09' + warnings.warn( + f'The variable "{variable}" is only available at 1 degree resolution, ' + f'annual time period, and "{pref}".' + ) + return ( + f'{base}/' + f'{pref}/' + f'{variable}_annual_1deg.nc' + ) else: - decav = 'decav' + dddd = 'decav' + pref = 'woa18' - v = { - 'temperature': 't', - 'silicate': 'i', - 'salinity': 's', - 'phosphate': 'p', - 'oxygen': 'o', - 'o2sat': 'O', - 'nitrate': 'n', - 'AOU': 'A' + grids = { + '5': ('5deg', '5d'), + '1': ('1.00', '01'), + '1/4': ('0.25', '04'), + } + grid = grids.get(resolution) + if not grid: + raise ValueError( + f'Unrecognizable resolution. Expected one of {list(grids.keys())}, got "{resolution}".' + ) + res = grid[0] + gg = grid[1] + + time_periods = { + 'annual': '00', + 'january': '01', + 'february': '02', + 'march': '03', + 'april': '04', + 'may': '05', + 'june': '06', + 'july': '07', + 'august': '08', + 'september': '09', + 'october': '10', + 'november': '11', + 'december': '12', + 'winter': '13', + 'spring': '14', + 'summer': '15', + 'autumn': '16', } - r = {'1.00': '1', '0.25': '4'} + time_period = time_period.lower() + if len(time_period) == 3: + tt = [time_periods.get(k) for k in time_periods.keys() if k.startswith(time_period)][0] + elif len(time_period) == 2 and time_period in time_periods.values(): + tt = time_period + else: + tt = time_periods.get(time_period) - var = v[variable] - res = r[resolution] + if not tt: + raise ValueError( + f'Unrecognizable time_period. ' + f'Expected one of {list(time_periods.keys())}, got "{time_period}".' + ) url = ( - f'https://data.nodc.noaa.gov/thredds/dodsC/woa/WOA13/DATA/' - f'{variable}/netcdf/{decav}/{resolution}/woa13_{decav}_{var}' - f'{clim_type}_0{res}.nc') - - with warnings.catch_warnings(): - warnings.simplefilter('ignore') - cubes = iris.load_raw(url) - cubes = [cube.intersection(longitude=(bbox[0], bbox[1]), - latitude=(bbox[2], bbox[3])) for cube in cubes] - cubes = iris.cube.CubeList(cubes) - if full: - return cubes - else: - cubes = [c for c in cubes if c.var_name == '{}_an'.format(var)] - return cubes[0] + f'{base}/' + '/ncei/woa/' + f'{variable}/decav/{res}/' + f'{pref}_{dddd}_{v}{tt}_{gg}.nc' # '[PREF]_[DDDD]_[V][TT][FF][GG]' Is [FF] used? + ) + return url -def woa_profile(lon, lat, variable='temperature', clim_type='00', resolution='1.00'): +def woa_profile(lon, lat, variable='temperature', time_period='annual', resolution='1'): """ - Return an iris.cube instance from a World Ocean Atlas 2013 variable at a + Return an iris.cube instance from a World Ocean Atlas variable at a given lon, lat point. Parameters ---------- lon, lat: float - point positions to extract the profile. + point positions to extract the interpolated profile. Choose data `variable` from: - 'temperature', 'silicate', 'salinity', 'phosphate', - 'oxygen', 'o2sat', 'nitrate', and 'AOU'. - Choose `clim_type` averages from: - 01-12 :: monthly - 13-16 :: seasonal (North Hemisphere Winter, Spring, Summer, - and Autumn respectively) - 00 :: annual + 'temperature', 'salinity', 'silicate', 'phosphate', + 'nitrate', 'oxygen_saturation', 'dissolved_oxygen', or + 'apparent_oxygen_utilization'. + Choose `time_period` from: + 01-12: January to December + 13-16: seasonal (North Hemisphere `Winter`, `Spring`, `Summer`, and `Autumn` respectively) + 00: Annual Choose `resolution` from: - 1 (1 degree), or 4 (0.25 degrees) + '5', '1', or '1/4' degrees (str) Returns ------- @@ -163,7 +132,7 @@ def woa_profile(lon, lat, variable='temperature', clim_type='00', resolution='1. >>> import matplotlib.pyplot as plt >>> from oceans.datasets import woa_profile >>> cube = woa_profile(-143, 10, variable='temperature', - ... clim_type='00', resolution='1.00') + ... time_period='annual', resolution='5') >>> fig, ax = plt.subplots(figsize=(2.25, 5)) >>> z = cube.coord(axis='Z').points >>> l = ax.plot(cube[0, :].data, z) @@ -172,41 +141,15 @@ def woa_profile(lon, lat, variable='temperature', clim_type='00', resolution='1. """ import iris - - if variable not in ['salinity', 'temperature']: - resolution = '1.00' - decav = 'all' - msg = '{} is only available at 1 degree resolution'.format - warnings.warn(msg(variable)) - else: - decav = 'decav' - - v = { - 'temperature': 't', - 'silicate': 'i', - 'salinity': 's', - 'phosphate': 'p', - 'oxygen': 'o', - 'o2sat': 'O', - 'nitrate': 'n', - 'AOU': 'A' - } - - r = {'1.00': '1', '0.25': '4'} - - var = v[variable] - res = r[resolution] - - url = ( - f'https://data.nodc.noaa.gov/thredds/dodsC/woa/WOA13/DATAv2/' - f'{variable}/netcdf/{decav}/{resolution}/woa13_{decav}_{var}' - f'{clim_type}_0{res}v2.nc') + url = _woa_url(variable=variable, time_period=time_period, resolution=resolution) with warnings.catch_warnings(): warnings.simplefilter('ignore') cubes = iris.load_raw(url) - cube = [c for c in cubes if c.var_name == '{}_an'.format(var)][0] + # TODO: should we be using `an` instead of `mn`? + v = _woa_variable(variable) + cube = [c for c in cubes if c.var_name == f'{v}_mn'][0] scheme = iris.analysis.Nearest() sample_points = [('longitude', lon), ('latitude', lat)] kw = { @@ -214,11 +157,77 @@ def woa_profile(lon, lat, variable='temperature', clim_type='00', resolution='1. 'scheme': scheme, 'collapse_scalar': True } - return cube.interpolate(**kw) -def etopo_subset(bbox=[-43, -30, -22, -17], tfile=None, smoo=False): +def woa_subset(bbox, variable='temperature', time_period='annual', resolution='5', full=False): + """ + Return an iris.cube instance from a World Ocean Atlas variable at a + given lon, lat bounding box. + + Parameters + ---------- + bbox: list, tuple + minx, maxx, miny, maxy positions to extract. + See `woa_profile` for the other options. + + Returns + ------- + `iris.Cube` instance with the climatology. + + Examples + -------- + >>> # Extract a 2D surface -- Annual temperature climatology: + >>> import iris.plot as iplt + >>> import matplotlib.pyplot as plt + >>> from oceans.colormaps import cm + >>> bbox = [2.5, 357.5, -87.5, 87.5] + >>> cube = woa_subset(bbox, variable='temperature', time_period='annual', resolution='5') + >>> c = cube[0, 0, ...] # Slice singleton time and first level. + >>> cs = iplt.pcolormesh(c, cmap=cm.avhrr) + >>> cbar = plt.colorbar(cs) + + >>> # Extract a square around the Mariana Trench averaging into a profile. + >>> import iris + >>> from oceans.colormaps import get_color + >>> colors = get_color(12) + >>> months = 'Jan Feb Apr Mar May Jun Jul Aug Sep Oct Nov Dec'.split() + >>> bbox = [-143, -141, 10, 12] + >>> fig, ax = plt.subplots(figsize=(5, 5)) + >>> for month in months: + ... cube = woa_subset(bbox, time_period=month, variable='temperature', resolution='1') + ... grid_areas = iris.analysis.cartography.area_weights(cube) + ... c = cube.collapsed(['longitude', 'latitude'], iris.analysis.MEAN, weights=grid_areas) + ... z = c.coord(axis='Z').points + ... l = ax.plot(c[0, :].data, z, label=month, color=next(colors)) + >>> ax.grid(True) + >>> ax.invert_yaxis() + >>> leg = ax.legend(loc='lower left') + >>> _ = ax.set_ylim(200, 0) + + """ + import iris + + v = _woa_variable(variable) + url = _woa_url(variable, time_period, resolution) + cubes = iris.load_raw(url) + cubes = [ + cube.intersection( + longitude=(bbox[0], bbox[1]), + latitude=(bbox[2], bbox[3])) for cube in cubes + ] + + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + cubes = iris.cube.CubeList(cubes) + + if full: + return cubes + else: + return [c for c in cubes if c.var_name == f'{v}_mn'][0] + + +def etopo_subset(bbox, tfile=None, smoo=False): """ Get a etopo subset. Should work on any netCDF with x, y, data @@ -235,7 +244,7 @@ def etopo_subset(bbox=[-43, -30, -22, -17], tfile=None, smoo=False): """ if tfile is None: - tfile = 'http://opendap.ccst.inpe.br/Misc/etopo2/ETOPO2v2c_f4.nc' + tfile = 'http://gamone.whoi.edu/thredds/dodsC/usgs/data0/bathy/ETOPO2v2c_f4.nc' with Dataset(tfile, 'r') as etopo: lons = etopo.variables['x'][:] diff --git a/oceans/ocfis/ocfis.py b/oceans/ocfis/ocfis.py index c140238..b5dbc00 100644 --- a/oceans/ocfis/ocfis.py +++ b/oceans/ocfis/ocfis.py @@ -94,7 +94,7 @@ def uv2spdir(u, v, mag=0, rot=0): def del_eta_del_x(U, f, g, balance='geostrophic', R=None): - """ + r""" Calculate :mat: `\frac{\partial \eta} {\partial x}` for different force balances @@ -126,7 +126,7 @@ def del_eta_del_x(U, f, g, balance='geostrophic', R=None): def mld(SA, CT, p, criterion='pdvar'): - """ + r""" Compute the mixed layer depth. Parameters diff --git a/oceans/sw_extras/sw_extras.py b/oceans/sw_extras/sw_extras.py index ba6c3ee..8d2a4b3 100644 --- a/oceans/sw_extras/sw_extras.py +++ b/oceans/sw_extras/sw_extras.py @@ -206,7 +206,7 @@ def cph(bvfr2): def shear(z, u, v=0): - """ + r""" Calculates the vertical shear for u, v velocity section. .. math:: @@ -255,7 +255,7 @@ def shear(z, u, v=0): def richnumb(bvfr2, S2): - """ + r""" Calculates the ratio of buoyancy to inertial forces which measures the stability of a fluid layer. this functions computes the gradient Richardson number in the form of: @@ -501,7 +501,7 @@ def tcond(s, t, p): def spice(s, t, p): - """ + r""" Compute sea spiciness as defined by Flament (2002). .. math:: \pi(\theta,s) = \sum^5_{i=0} \sum^4_{j=0} b_{ij}\theta^i(s-35)^i diff --git a/oceans/sw_extras/waves.py b/oceans/sw_extras/waves.py index 95be00d..7484c0e 100644 --- a/oceans/sw_extras/waves.py +++ b/oceans/sw_extras/waves.py @@ -4,7 +4,7 @@ class Waves(object): - """ + r""" Solves the wave dispersion relationship via Newton-Raphson. .. math:: diff --git a/setup.py b/setup.py index a42367d..b3062af 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,10 @@ def read(*parts): 'Operating System :: OS Independent', 'Programming Language :: Python', 'Topic :: Education', - 'Topic :: Scientific/Engineering' + 'Topic :: Scientific/Engineering', + 'Programming Language :: Python :: 3 :: Only', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', ], description='Misc functions for oceanographic data analysis', author=authors, diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000