From fb92854d790c84ac0897911b4d7a50469dd6678c Mon Sep 17 00:00:00 2001 From: Spencer Clark Date: Sat, 19 May 2018 07:57:48 -0400 Subject: [PATCH 01/17] Tests pass --- aospy/data_loader.py | 14 +- aospy/examples/example_obj_lib.py | 7 +- aospy/examples/tutorial.ipynb | 201 ++++++++++++++++------------ aospy/test/data/objects/examples.py | 7 +- aospy/test/test_calc_basic.py | 15 ++- aospy/test/test_data_loader.py | 53 +++++--- aospy/test/test_utils_times.py | 8 +- aospy/utils/times.py | 17 ++- 8 files changed, 183 insertions(+), 139 deletions(-) diff --git a/aospy/data_loader.py b/aospy/data_loader.py index 3962eb9..df8a779 100644 --- a/aospy/data_loader.py +++ b/aospy/data_loader.py @@ -1,7 +1,6 @@ """aospy DataLoader objects""" import logging import os -import warnings import numpy as np import xarray as xr @@ -180,7 +179,6 @@ def _prep_time_data(ds): The processed Dataset and minimum and maximum years in the loaded data """ ds = times.ensure_time_as_index(ds) - ds, min_year, max_year = times.numpy_datetime_workaround_encode_cf(ds) if TIME_BOUNDS_STR in ds: ds = times.ensure_time_avg_has_cf_metadata(ds) ds[TIME_STR] = times.average_time_bounds(ds) @@ -189,10 +187,10 @@ def _prep_time_data(ds): "values in time, even though this may not be " "the case") ds = times.add_uniform_time_weights(ds) - with warnings.catch_warnings(record=True): + with xr.set_options(enable_cftimeindex=True): ds = xr.decode_cf(ds, decode_times=True, decode_coords=False, mask_and_scale=True) - return ds, min_year, max_year + return ds def _load_data_from_disk(file_set, preprocess_func=lambda ds: ds, @@ -281,16 +279,12 @@ def load_variable(self, var=None, start_date=None, end_date=None, time_offset=time_offset, **DataAttrs ) - ds, min_year, max_year = _prep_time_data(ds) + ds = _prep_time_data(ds) ds = set_grid_attrs_as_coords(ds) da = _sel_var(ds, var, self.upcast_float32) da = self._maybe_apply_time_shift(da, time_offset, **DataAttrs) - start_date_xarray = times.numpy_datetime_range_workaround( - start_date, min_year, max_year) - end_date_xarray = start_date_xarray + (end_date - start_date) - return times.sel_time(da, np.datetime64(start_date_xarray), - np.datetime64(end_date_xarray)).load() + return times.sel_time(da, start_date, end_date).load() def recursively_compute_variable(self, var, start_date=None, end_date=None, time_offset=None, **DataAttrs): diff --git a/aospy/examples/example_obj_lib.py b/aospy/examples/example_obj_lib.py index a71f934..8387a45 100644 --- a/aospy/examples/example_obj_lib.py +++ b/aospy/examples/example_obj_lib.py @@ -1,7 +1,8 @@ """Sample aospy object library using the included example data.""" -import datetime import os +from cftime import DatetimeNoLeap + import aospy from aospy import Model, Proj, Region, Run, Var from aospy.data_loader import DictDataLoader @@ -17,8 +18,8 @@ description=( 'Control simulation of the idealized moist model' ), - default_start_date=datetime.datetime(4, 1, 1), - default_end_date=datetime.datetime(6, 12, 31), + default_start_date=DatetimeNoLeap(4, 1, 1), + default_end_date=DatetimeNoLeap(6, 12, 31), data_loader=DictDataLoader(_file_map) ) diff --git a/aospy/examples/tutorial.ipynb b/aospy/examples/tutorial.ipynb index 6d234ce..10535e4 100644 --- a/aospy/examples/tutorial.ipynb +++ b/aospy/examples/tutorial.ipynb @@ -21,9 +21,7 @@ { "cell_type": "code", "execution_count": 1, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "import os # Python built-in package for working with the operating system\n", @@ -56,10 +54,11 @@ " * nv (nv) float64 1.0 2.0\n", " * time (time) float64 1.111e+03 1.139e+03 1.17e+03 1.2e+03 ...\n", "Data variables:\n", - " condensation_rain (time, lat, lon) float64 5.768e-06 5.784e-06 ...\n", - " convection_rain (time, lat, lon) float64 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ...\n", - " time_bounds (time, nv) float64 1.095e+03 1.126e+03 1.126e+03 ...\n", - " average_DT (time) float64 31.0 28.0 31.0 30.0 31.0 30.0 31.0 ..." + " condensation_rain (time, lat, lon) float32 dask.array\n", + " convection_rain (time, lat, lon) float32 dask.array\n", + " time_bounds (time, nv) float64 dask.array\n", + " average_DT (time) float64 dask.array\n", + " zsurf (time, lat, lon) float32 dask.array" ] }, "execution_count": 2, @@ -107,9 +106,7 @@ { "cell_type": "code", "execution_count": 3, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "from aospy.data_loader import DictDataLoader\n", @@ -129,9 +126,7 @@ { "cell_type": "code", "execution_count": 4, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "from aospy import Run\n", @@ -159,9 +154,7 @@ { "cell_type": "code", "execution_count": 5, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "from aospy import Model\n", @@ -188,9 +181,7 @@ { "cell_type": "code", "execution_count": 6, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "from aospy import Proj\n", @@ -227,9 +218,7 @@ { "cell_type": "code", "execution_count": 7, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "from aospy import Var\n", @@ -262,9 +251,7 @@ { "cell_type": "code", "execution_count": 8, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "def total_precip(condensation_rain, convection_rain):\n", @@ -315,9 +302,7 @@ { "cell_type": "code", "execution_count": 9, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "from aospy import Region\n", @@ -363,9 +348,7 @@ { "cell_type": "code", "execution_count": 10, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "from aospy.examples import example_obj_lib as lib\n", @@ -401,9 +384,7 @@ { "cell_type": "code", "execution_count": 11, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "calc_exec_options = dict(prompt_verify=False, parallelize=False,\n", @@ -426,56 +407,106 @@ "name": "stderr", "output_type": "stream", "text": [ - "INFO:root:Getting input data: Var instance \"precip_largescale\" (Thu Mar 30 08:49:11 2017)\n", - "/home/skc/miniconda/envs/research/lib/python3.6/site-packages/xarray/conventions.py:389: RuntimeWarning: Unable to decode time axis into full numpy.datetime64 objects, continuing using dummy netCDF4.datetime objects instead, reason: dates out of range\n", - " result = decode_cf_datetime(example_value, units, calendar)\n", - "/home/skc/miniconda/envs/research/lib/python3.6/site-packages/xarray/conventions.py:408: RuntimeWarning: Unable to decode time axis into full numpy.datetime64 objects, continuing using dummy netCDF4.datetime objects instead, reason: dates out of range\n", - " calendar=self.calendar)\n", - "INFO:root:Getting input data: Var instance \"precip_convective\" (Thu Mar 30 08:49:11 2017)\n", - "/home/skc/miniconda/envs/research/lib/python3.6/site-packages/xarray/conventions.py:389: RuntimeWarning: Unable to decode time axis into full numpy.datetime64 objects, continuing using dummy netCDF4.datetime objects instead, reason: dates out of range\n", - " result = decode_cf_datetime(example_value, units, calendar)\n", - "/home/skc/miniconda/envs/research/lib/python3.6/site-packages/xarray/conventions.py:408: RuntimeWarning: Unable to decode time axis into full numpy.datetime64 objects, continuing using dummy netCDF4.datetime objects instead, reason: dates out of range\n", - " calendar=self.calendar)\n", - "INFO:root:Computing timeseries for 0004-01-01 00:00:00 -- 0006-12-31 00:00:00.\n", - "INFO:root:Applying desired time-reduction methods. (Thu Mar 30 08:49:11 2017)\n", - "INFO:root:Writing desired gridded outputs to disk.\n", - "INFO:root:\texample-output/example_proj/example_model/example_run/precip_conv_frac/precip_conv_frac.ann.av.from_monthly_ts.example_model.example_run.0004-0006.nc\n", - "INFO:root:\texample-output/example_proj/example_model/example_run/precip_conv_frac/precip_conv_frac.ann.reg.av.from_monthly_ts.example_model.example_run.0004-0006.nc\n", - "INFO:root:Getting input data: Var instance \"precip_largescale\" (Thu Mar 30 08:49:12 2017)\n", - "/home/skc/miniconda/envs/research/lib/python3.6/site-packages/xarray/conventions.py:389: RuntimeWarning: Unable to decode time axis into full numpy.datetime64 objects, continuing using dummy netCDF4.datetime objects instead, reason: dates out of range\n", - " result = decode_cf_datetime(example_value, units, calendar)\n", - "/home/skc/miniconda/envs/research/lib/python3.6/site-packages/xarray/conventions.py:408: RuntimeWarning: Unable to decode time axis into full numpy.datetime64 objects, continuing using dummy netCDF4.datetime objects instead, reason: dates out of range\n", - " calendar=self.calendar)\n", - "INFO:root:Getting input data: Var instance \"precip_convective\" (Thu Mar 30 08:49:12 2017)\n", - "/home/skc/miniconda/envs/research/lib/python3.6/site-packages/xarray/conventions.py:389: RuntimeWarning: Unable to decode time axis into full numpy.datetime64 objects, continuing using dummy netCDF4.datetime objects instead, reason: dates out of range\n", - " result = decode_cf_datetime(example_value, units, calendar)\n", - "/home/skc/miniconda/envs/research/lib/python3.6/site-packages/xarray/conventions.py:408: RuntimeWarning: Unable to decode time axis into full numpy.datetime64 objects, continuing using dummy netCDF4.datetime objects instead, reason: dates out of range\n", - " calendar=self.calendar)\n", - "INFO:root:Computing timeseries for 0004-01-01 00:00:00 -- 0006-12-31 00:00:00.\n", - "INFO:root:Applying desired time-reduction methods. (Thu Mar 30 08:49:13 2017)\n", - "INFO:root:Writing desired gridded outputs to disk.\n", - "INFO:root:\texample-output/example_proj/example_model/example_run/precip_total/precip_total.ann.av.from_monthly_ts.example_model.example_run.0004-0006.nc\n", - "INFO:root:\texample-output/example_proj/example_model/example_run/precip_total/precip_total.ann.reg.av.from_monthly_ts.example_model.example_run.0004-0006.nc\n", - "INFO:root:Getting input data: Var instance \"precip_convective\" (Thu Mar 30 08:49:13 2017)\n", - "/home/skc/miniconda/envs/research/lib/python3.6/site-packages/xarray/conventions.py:389: RuntimeWarning: Unable to decode time axis into full numpy.datetime64 objects, continuing using dummy netCDF4.datetime objects instead, reason: dates out of range\n", - " result = decode_cf_datetime(example_value, units, calendar)\n", - "/home/skc/miniconda/envs/research/lib/python3.6/site-packages/xarray/conventions.py:408: RuntimeWarning: Unable to decode time axis into full numpy.datetime64 objects, continuing using dummy netCDF4.datetime objects instead, reason: dates out of range\n", - " calendar=self.calendar)\n", - "INFO:root:Computing timeseries for 0004-01-01 00:00:00 -- 0006-12-31 00:00:00.\n", - "INFO:root:Applying desired time-reduction methods. (Thu Mar 30 08:49:13 2017)\n", - "INFO:root:Writing desired gridded outputs to disk.\n", - "INFO:root:\texample-output/example_proj/example_model/example_run/precip_convective/precip_convective.ann.av.from_monthly_ts.example_model.example_run.0004-0006.nc\n", - "INFO:root:\texample-output/example_proj/example_model/example_run/precip_convective/precip_convective.ann.reg.av.from_monthly_ts.example_model.example_run.0004-0006.nc\n", - "INFO:root:Getting input data: Var instance \"precip_largescale\" (Thu Mar 30 08:49:13 2017)\n", - "/home/skc/miniconda/envs/research/lib/python3.6/site-packages/xarray/conventions.py:389: RuntimeWarning: Unable to decode time axis into full numpy.datetime64 objects, continuing using dummy netCDF4.datetime objects instead, reason: dates out of range\n", - " result = decode_cf_datetime(example_value, units, calendar)\n", - "/home/skc/miniconda/envs/research/lib/python3.6/site-packages/xarray/conventions.py:408: RuntimeWarning: Unable to decode time axis into full numpy.datetime64 objects, continuing using dummy netCDF4.datetime objects instead, reason: dates out of range\n", - " calendar=self.calendar)\n", - "INFO:root:Computing timeseries for 0004-01-01 00:00:00 -- 0006-12-31 00:00:00.\n", - "INFO:root:Applying desired time-reduction methods. (Thu Mar 30 08:49:13 2017)\n", - "INFO:root:Writing desired gridded outputs to disk.\n", - "INFO:root:\texample-output/example_proj/example_model/example_run/precip_largescale/precip_largescale.ann.av.from_monthly_ts.example_model.example_run.0004-0006.nc\n", - "INFO:root:\texample-output/example_proj/example_model/example_run/precip_largescale/precip_largescale.ann.reg.av.from_monthly_ts.example_model.example_run.0004-0006.nc\n" + "INFO:root:Getting input data: Var instance \"precip_largescale\" (Sat May 19 07:19:45 2018)\n", + "WARNING:root:Datapoints were stored using the np.float32 datatype.For accurate reduction operations using bottleneck, datapoints are being cast to the np.float64 datatype. For more information see: https://github.com/pydata/xarray/issues/1346\n", + "WARNING:root:Skipping aospy calculation `` due to error with the following traceback: \n", + "Traceback (most recent call last):\n", + " File \"/Users/spencerclark/aospy/aospy/automate.py\", line 270, in _compute_or_skip_on_error\n", + " return calc.compute(**compute_kwargs)\n", + " File \"/Users/spencerclark/aospy/aospy/calc.py\", line 569, in compute\n", + " self.end_date),\n", + " File \"/Users/spencerclark/aospy/aospy/calc.py\", line 415, in _get_all_data\n", + " for n, var in enumerate(self.variables)]\n", + " File \"/Users/spencerclark/aospy/aospy/calc.py\", line 415, in \n", + " for n, var in enumerate(self.variables)]\n", + " File \"/Users/spencerclark/aospy/aospy/calc.py\", line 367, in _get_input_data\n", + " **self.data_loader_attrs)\n", + " File \"/Users/spencerclark/aospy/aospy/data_loader.py\", line 318, in recursively_compute_variable\n", + " **DataAttrs)\n", + " File \"/Users/spencerclark/aospy/aospy/data_loader.py\", line 287, in load_variable\n", + " return times.sel_time(da, start_date, end_date).load()\n", + " File \"/Users/spencerclark/aospy/aospy/utils/times.py\", line 515, in sel_time\n", + " _assert_has_data_for_time(da, start_date, end_date)\n", + " File \"/Users/spencerclark/aospy/aospy/utils/times.py\", line 484, in _assert_has_data_for_time\n", + " range_exists = start_date >= da_start and end_date <= da_end\n", + " File \"cftime/_cftime.pyx\", line 1615, in cftime._cftime.datetime.__richcmp__\n", + "TypeError: cannot compare cftime.DatetimeNoLeap(4, 1, 1, 0, 0, 0, 0, 6, 1) and datetime.datetime(4, 1, 1, 0, 0) (different calendars)\n", + "\n", + "INFO:root:Getting input data: Var instance \"precip_convective\" (Sat May 19 07:19:45 2018)\n", + "WARNING:root:Datapoints were stored using the np.float32 datatype.For accurate reduction operations using bottleneck, datapoints are being cast to the np.float64 datatype. For more information see: https://github.com/pydata/xarray/issues/1346\n", + "WARNING:root:Skipping aospy calculation `` due to error with the following traceback: \n", + "Traceback (most recent call last):\n", + " File \"/Users/spencerclark/aospy/aospy/automate.py\", line 270, in _compute_or_skip_on_error\n", + " return calc.compute(**compute_kwargs)\n", + " File \"/Users/spencerclark/aospy/aospy/calc.py\", line 569, in compute\n", + " self.end_date),\n", + " File \"/Users/spencerclark/aospy/aospy/calc.py\", line 415, in _get_all_data\n", + " for n, var in enumerate(self.variables)]\n", + " File \"/Users/spencerclark/aospy/aospy/calc.py\", line 415, in \n", + " for n, var in enumerate(self.variables)]\n", + " File \"/Users/spencerclark/aospy/aospy/calc.py\", line 367, in _get_input_data\n", + " **self.data_loader_attrs)\n", + " File \"/Users/spencerclark/aospy/aospy/data_loader.py\", line 318, in recursively_compute_variable\n", + " **DataAttrs)\n", + " File \"/Users/spencerclark/aospy/aospy/data_loader.py\", line 287, in load_variable\n", + " return times.sel_time(da, start_date, end_date).load()\n", + " File \"/Users/spencerclark/aospy/aospy/utils/times.py\", line 515, in sel_time\n", + " _assert_has_data_for_time(da, start_date, end_date)\n", + " File \"/Users/spencerclark/aospy/aospy/utils/times.py\", line 484, in _assert_has_data_for_time\n", + " range_exists = start_date >= da_start and end_date <= da_end\n", + " File \"cftime/_cftime.pyx\", line 1615, in cftime._cftime.datetime.__richcmp__\n", + "TypeError: cannot compare cftime.DatetimeNoLeap(4, 1, 1, 0, 0, 0, 0, 6, 1) and datetime.datetime(4, 1, 1, 0, 0) (different calendars)\n", + "\n", + "INFO:root:Getting input data: Var instance \"precip_largescale\" (Sat May 19 07:19:45 2018)\n", + "WARNING:root:Datapoints were stored using the np.float32 datatype.For accurate reduction operations using bottleneck, datapoints are being cast to the np.float64 datatype. For more information see: https://github.com/pydata/xarray/issues/1346\n", + "WARNING:root:Skipping aospy calculation `` due to error with the following traceback: \n", + "Traceback (most recent call last):\n", + " File \"/Users/spencerclark/aospy/aospy/automate.py\", line 270, in _compute_or_skip_on_error\n", + " return calc.compute(**compute_kwargs)\n", + " File \"/Users/spencerclark/aospy/aospy/calc.py\", line 569, in compute\n", + " self.end_date),\n", + " File \"/Users/spencerclark/aospy/aospy/calc.py\", line 415, in _get_all_data\n", + " for n, var in enumerate(self.variables)]\n", + " File \"/Users/spencerclark/aospy/aospy/calc.py\", line 415, in \n", + " for n, var in enumerate(self.variables)]\n", + " File \"/Users/spencerclark/aospy/aospy/calc.py\", line 367, in _get_input_data\n", + " **self.data_loader_attrs)\n", + " File \"/Users/spencerclark/aospy/aospy/data_loader.py\", line 318, in recursively_compute_variable\n", + " **DataAttrs)\n", + " File \"/Users/spencerclark/aospy/aospy/data_loader.py\", line 287, in load_variable\n", + " return times.sel_time(da, start_date, end_date).load()\n", + " File \"/Users/spencerclark/aospy/aospy/utils/times.py\", line 515, in sel_time\n", + " _assert_has_data_for_time(da, start_date, end_date)\n", + " File \"/Users/spencerclark/aospy/aospy/utils/times.py\", line 484, in _assert_has_data_for_time\n", + " range_exists = start_date >= da_start and end_date <= da_end\n", + " File \"cftime/_cftime.pyx\", line 1615, in cftime._cftime.datetime.__richcmp__\n", + "TypeError: cannot compare cftime.DatetimeNoLeap(4, 1, 1, 0, 0, 0, 0, 6, 1) and datetime.datetime(4, 1, 1, 0, 0) (different calendars)\n", + "\n", + "INFO:root:Getting input data: Var instance \"precip_largescale\" (Sat May 19 07:19:45 2018)\n", + "WARNING:root:Datapoints were stored using the np.float32 datatype.For accurate reduction operations using bottleneck, datapoints are being cast to the np.float64 datatype. For more information see: https://github.com/pydata/xarray/issues/1346\n", + "WARNING:root:Skipping aospy calculation `` due to error with the following traceback: \n", + "Traceback (most recent call last):\n", + " File \"/Users/spencerclark/aospy/aospy/automate.py\", line 270, in _compute_or_skip_on_error\n", + " return calc.compute(**compute_kwargs)\n", + " File \"/Users/spencerclark/aospy/aospy/calc.py\", line 569, in compute\n", + " self.end_date),\n", + " File \"/Users/spencerclark/aospy/aospy/calc.py\", line 415, in _get_all_data\n", + " for n, var in enumerate(self.variables)]\n", + " File \"/Users/spencerclark/aospy/aospy/calc.py\", line 415, in \n", + " for n, var in enumerate(self.variables)]\n", + " File \"/Users/spencerclark/aospy/aospy/calc.py\", line 367, in _get_input_data\n", + " **self.data_loader_attrs)\n", + " File \"/Users/spencerclark/aospy/aospy/data_loader.py\", line 318, in recursively_compute_variable\n", + " **DataAttrs)\n", + " File \"/Users/spencerclark/aospy/aospy/data_loader.py\", line 287, in load_variable\n", + " return times.sel_time(da, start_date, end_date).load()\n", + " File \"/Users/spencerclark/aospy/aospy/utils/times.py\", line 515, in sel_time\n", + " _assert_has_data_for_time(da, start_date, end_date)\n", + " File \"/Users/spencerclark/aospy/aospy/utils/times.py\", line 484, in _assert_has_data_for_time\n", + " range_exists = start_date >= da_start and end_date <= da_end\n", + " File \"cftime/_cftime.pyx\", line 1615, in cftime._cftime.datetime.__richcmp__\n", + "TypeError: cannot compare cftime.DatetimeNoLeap(4, 1, 1, 0, 0, 0, 0, 6, 1) and datetime.datetime(4, 1, 1, 0, 0) (different calendars)\n", + "\n" ] } ], @@ -822,7 +853,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.3" + "version": "3.6.1" }, "latex_envs": { "LaTeX_envs_menu_present": true, diff --git a/aospy/test/data/objects/examples.py b/aospy/test/data/objects/examples.py index 7ddb311..6087f9a 100644 --- a/aospy/test/data/objects/examples.py +++ b/aospy/test/data/objects/examples.py @@ -1,6 +1,7 @@ -from datetime import datetime import os +from cftime import DatetimeNoLeap + from aospy import Proj, Model, Run, Var, Region from aospy.data_loader import NestedDictDataLoader @@ -25,8 +26,8 @@ def total_precipitation(convection_rain, condensation_rain): 'Control simulation of the idealized moist model' ), data_loader=NestedDictDataLoader(file_map), - default_start_date=datetime(4, 1, 1), - default_end_date=datetime(6, 12, 31) + default_start_date=DatetimeNoLeap(4, 1, 1), + default_end_date=DatetimeNoLeap(6, 12, 31) ) example_model = Model( diff --git a/aospy/test/test_calc_basic.py b/aospy/test/test_calc_basic.py index b36e0aa..b630abc 100755 --- a/aospy/test/test_calc_basic.py +++ b/aospy/test/test_calc_basic.py @@ -1,6 +1,5 @@ #!/usr/bin/env python """Basic test of the Calc module on 2D data.""" -import datetime from os.path import isfile import shutil import unittest @@ -9,6 +8,8 @@ import xarray as xr +from cftime import DatetimeNoLeap + from aospy import Var from aospy.calc import Calc, _add_metadata_as_attrs from .data.objects.examples import ( @@ -44,8 +45,8 @@ def _test_files_and_attrs(calc, dtype_out): 'model': example_model, 'run': example_run, 'var': condensation_rain, - 'date_range': (datetime.datetime(4, 1, 1), - datetime.datetime(6, 12, 31)), + 'date_range': (DatetimeNoLeap(4, 1, 1), + DatetimeNoLeap(6, 12, 31)), 'intvl_in': 'monthly', 'dtype_in_time': 'ts' } @@ -118,8 +119,8 @@ def setUp(self): 'model': example_model, 'run': example_run, 'var': precip, - 'date_range': (datetime.datetime(4, 1, 1), - datetime.datetime(6, 12, 31)), + 'date_range': (DatetimeNoLeap(4, 1, 1), + DatetimeNoLeap(6, 12, 31)), 'intvl_in': 'monthly', 'dtype_in_time': 'ts' } @@ -132,8 +133,8 @@ def setUp(self): 'model': example_model, 'run': example_run, 'var': sphum, - 'date_range': (datetime.datetime(6, 1, 1), - datetime.datetime(6, 1, 31)), + 'date_range': (DatetimeNoLeap(6, 1, 1), + DatetimeNoLeap(6, 1, 31)), 'intvl_in': 'monthly', 'dtype_in_time': 'ts', 'dtype_in_vert': 'sigma', diff --git a/aospy/test/test_data_loader.py b/aospy/test/test_data_loader.py index 852ea7c..fae482c 100644 --- a/aospy/test/test_data_loader.py +++ b/aospy/test/test_data_loader.py @@ -9,6 +9,8 @@ import pytest import xarray as xr +from cftime import DatetimeNoLeap + from aospy import Var from aospy.data_loader import (DataLoader, DictDataLoader, GFDLDataLoader, NestedDictDataLoader, grid_attrs_to_aospy_names, @@ -169,10 +171,8 @@ def test_generate_file_set(self): def test_prep_time_data(self): assert (TIME_WEIGHTS_STR not in self.inst_ds) - ds, min_year, max_year = _prep_time_data(self.inst_ds) + ds = _prep_time_data(self.inst_ds) assert (TIME_WEIGHTS_STR in ds) - self.assertEqual(min_year, 2000) - self.assertEqual(max_year, 2000) def test_preprocess_and_rename_grid_attrs(self): def preprocess_func(ds, **kwargs): @@ -473,7 +473,8 @@ def tearDown(self): def test_load_variable(self): result = self.data_loader.load_variable( - condensation_rain, datetime(5, 1, 1), datetime(5, 12, 31), + condensation_rain, DatetimeNoLeap(5, 1, 1), + DatetimeNoLeap(5, 12, 31), intvl_in='monthly') filepath = os.path.join(os.path.split(ROOT_PATH)[0], 'netcdf', '00050101.precip_monthly.nc') @@ -483,8 +484,8 @@ def test_load_variable(self): def test_load_variable_does_not_warn(self): with warnings.catch_warnings(record=True) as warnlog: self.data_loader.load_variable(condensation_rain, - datetime(5, 1, 1), - datetime(5, 12, 31), + DatetimeNoLeap(5, 1, 1), + DatetimeNoLeap(5, 12, 31), intvl_in='monthly') assert len(warnlog) == 0 @@ -494,7 +495,8 @@ def preprocess(ds, **kwargs): return ds.astype(np.float32) self.data_loader.preprocess_func = preprocess result = self.data_loader.load_variable( - condensation_rain, datetime(4, 1, 1), datetime(4, 12, 31), + condensation_rain, DatetimeNoLeap(4, 1, 1), + DatetimeNoLeap(4, 12, 31), intvl_in='monthly').dtype expected = np.float64 self.assertEqual(result, expected) @@ -506,7 +508,8 @@ def preprocess(ds, **kwargs): self.data_loader.preprocess_func = preprocess self.data_loader.upcast_float32 = False result = self.data_loader.load_variable( - condensation_rain, datetime(4, 1, 1), datetime(4, 12, 31), + condensation_rain, DatetimeNoLeap(4, 1, 1), + DatetimeNoLeap(4, 12, 31), intvl_in='monthly').dtype expected = np.float32 self.assertEqual(result, expected) @@ -524,14 +527,16 @@ def preprocess(ds, **kwargs): self.data_loader.data_vars = 'all' self.data_loader.preprocess_func = preprocess data = self.data_loader.load_variable( - condensation_rain, datetime(4, 1, 1), datetime(5, 12, 31), + condensation_rain, DatetimeNoLeap(4, 1, 1), + DatetimeNoLeap(5, 12, 31), intvl_in='monthly') result = TIME_STR in data.coords self.assertEqual(result, True) def test_load_variable_data_vars_default(self): data = self.data_loader.load_variable( - condensation_rain, datetime(4, 1, 1), datetime(5, 12, 31), + condensation_rain, DatetimeNoLeap(4, 1, 1), + DatetimeNoLeap(5, 12, 31), intvl_in='monthly') result = TIME_STR in data.coords self.assertEqual(result, True) @@ -539,7 +544,8 @@ def test_load_variable_data_vars_default(self): def test_load_variable_coords_all(self): self.data_loader.coords = 'all' data = self.data_loader.load_variable( - condensation_rain, datetime(4, 1, 1), datetime(5, 12, 31), + condensation_rain, DatetimeNoLeap(4, 1, 1), + DatetimeNoLeap(5, 12, 31), intvl_in='monthly') result = TIME_STR in data[ZSURF_STR].coords self.assertEqual(result, True) @@ -553,16 +559,18 @@ def preprocess(ds, **kwargs): three_yrs = 1095. ds['time'] = ds['time'] - three_yrs ds['time'].attrs['units'] = 'days since 0004-01-01 00:00:00' + ds['time'].attrs['calendar'] = 'noleap' ds['time_bounds'] = ds['time_bounds'] - three_yrs ds['time_bounds'].attrs['units'] = 'days since 0004-01-01 00:00:00' + ds['time_bounds'].attrs['calendar'] = 'noleap' return ds self.data_loader.preprocess_func = preprocess for year in [4, 5, 6]: result = self.data_loader.load_variable( - condensation_rain, datetime(year, 1, 1), - datetime(year, 12, 31), + condensation_rain, DatetimeNoLeap(year, 1, 1), + DatetimeNoLeap(year, 12, 31), intvl_in='monthly') filepath = os.path.join(os.path.split(ROOT_PATH)[0], 'netcdf', '000{}0101.precip_monthly.nc'.format(year)) @@ -571,14 +579,15 @@ def preprocess(ds, **kwargs): def test_load_variable_preprocess(self): def preprocess(ds, **kwargs): - if kwargs['start_date'] == datetime(5, 1, 1): + if kwargs['start_date'] == DatetimeNoLeap(5, 1, 1): ds['condensation_rain'] = 10. * ds['condensation_rain'] return ds self.data_loader.preprocess_func = preprocess result = self.data_loader.load_variable( - condensation_rain, datetime(5, 1, 1), datetime(5, 12, 31), + condensation_rain, DatetimeNoLeap(5, 1, 1), + DatetimeNoLeap(5, 12, 31), intvl_in='monthly') filepath = os.path.join(os.path.split(ROOT_PATH)[0], 'netcdf', '00050101.precip_monthly.nc') @@ -586,7 +595,8 @@ def preprocess(ds, **kwargs): np.testing.assert_allclose(result.values, expected.values) result = self.data_loader.load_variable( - condensation_rain, datetime(4, 1, 1), datetime(4, 12, 31), + condensation_rain, DatetimeNoLeap(4, 1, 1), + DatetimeNoLeap(4, 12, 31), intvl_in='monthly') filepath = os.path.join(os.path.split(ROOT_PATH)[0], 'netcdf', '00040101.precip_monthly.nc') @@ -602,8 +612,8 @@ def convert_all_to_missing_val(ds, **kwargs): self.data_loader.preprocess_func = convert_all_to_missing_val data = self.data_loader.load_variable( - condensation_rain, datetime(5, 1, 1), - datetime(5, 12, 31), + condensation_rain, DatetimeNoLeap(5, 1, 1), + DatetimeNoLeap(5, 12, 31), intvl_in='monthly') num_non_missing = np.isfinite(data).sum().item() @@ -612,7 +622,8 @@ def convert_all_to_missing_val(ds, **kwargs): def test_recursively_compute_variable_native(self): result = self.data_loader.recursively_compute_variable( - condensation_rain, datetime(5, 1, 1), datetime(5, 12, 31), + condensation_rain, DatetimeNoLeap(5, 1, 1), + DatetimeNoLeap(5, 12, 31), intvl_in='monthly') filepath = os.path.join(os.path.split(ROOT_PATH)[0], 'netcdf', '00050101.precip_monthly.nc') @@ -624,7 +635,7 @@ def test_recursively_compute_variable_one_level(self): name='one_level', variables=(condensation_rain, condensation_rain), func=lambda x, y: x + y) result = self.data_loader.recursively_compute_variable( - one_level, datetime(5, 1, 1), datetime(5, 12, 31), + one_level, DatetimeNoLeap(5, 1, 1), DatetimeNoLeap(5, 12, 31), intvl_in='monthly') filepath = os.path.join(os.path.split(ROOT_PATH)[0], 'netcdf', '00050101.precip_monthly.nc') @@ -639,7 +650,7 @@ def test_recursively_compute_variable_multi_level(self): name='multi_level', variables=(one_level, condensation_rain), func=lambda x, y: x + y) result = self.data_loader.recursively_compute_variable( - multi_level, datetime(5, 1, 1), datetime(5, 12, 31), + multi_level, DatetimeNoLeap(5, 1, 1), DatetimeNoLeap(5, 12, 31), intvl_in='monthly') filepath = os.path.join(os.path.split(ROOT_PATH)[0], 'netcdf', '00050101.precip_monthly.nc') diff --git a/aospy/test/test_utils_times.py b/aospy/test/test_utils_times.py index 00b4216..79f5b72 100755 --- a/aospy/test/test_utils_times.py +++ b/aospy/test/test_utils_times.py @@ -404,12 +404,12 @@ def test_assert_has_data_for_time(): ds = xr.decode_cf(ds) da = ds[var_name] - start_date = np.datetime64('2000-01-01') - end_date = np.datetime64('2000-03-31') + start_date = np.datetime64('2000-01-01', 'ns') + end_date = np.datetime64('2000-03-31', 'ns') _assert_has_data_for_time(da, start_date, end_date) - start_date_bad = np.datetime64('1999-12-31') - end_date_bad = np.datetime64('2000-04-01') + start_date_bad = np.datetime64('1999-12-31', 'ns') + end_date_bad = np.datetime64('2000-04-01', 'ns') with pytest.raises(AssertionError): _assert_has_data_for_time(da, start_date_bad, end_date) diff --git a/aospy/utils/times.py b/aospy/utils/times.py index 08b0c89..e1170f0 100644 --- a/aospy/utils/times.py +++ b/aospy/utils/times.py @@ -2,6 +2,7 @@ import datetime import warnings +import cftime import numpy as np import pandas as pd import xarray as xr @@ -173,7 +174,7 @@ def yearly_average(arr, dt): def ensure_datetime(obj): - """Return the object if it is of type datetime.datetime; else raise. + """Return the object if it is a datetime-like object Parameters ---------- @@ -181,15 +182,15 @@ def ensure_datetime(obj): Returns ------- - The original object if it is a datetime.datetime object. + The original object if it is a datetime-like object Raises ------ - TypeError if `obj` is not of type `datetime.datetime`. + TypeError if `obj` is not datetime-like """ - if isinstance(obj, datetime.datetime): + if isinstance(obj, (datetime.datetime, cftime.datetime, np.datetime64)): return obj - raise TypeError("`datetime.datetime` object required. " + raise TypeError("datetime-like object required. " "Type given: {}".format(type(obj))) @@ -480,7 +481,11 @@ def _assert_has_data_for_time(da, start_date, end_date): da_start, da_end = times.values message = ('Data does not exist for requested time range: {0} to {1};' ' found data from time range: {2} to {3}.') - range_exists = start_date >= da_start and end_date <= da_end + print(da_start) + print(da_end) + print(start_date) + print(end_date) + range_exists = da_start <= start_date and da_end >= end_date assert (range_exists), message.format(start_date, end_date, da_start, da_end) From 2f93cd806b1baaeb8656daca9d4d251b8747464d Mon Sep 17 00:00:00 2001 From: Spencer Clark Date: Sat, 19 May 2018 08:01:52 -0400 Subject: [PATCH 02/17] Use enable_cftimeindex=True when opening datasets in test_calc_basic.py --- aospy/test/test_calc_basic.py | 37 +++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/aospy/test/test_calc_basic.py b/aospy/test/test_calc_basic.py index b630abc..a44211b 100755 --- a/aospy/test/test_calc_basic.py +++ b/aospy/test/test_calc_basic.py @@ -19,19 +19,20 @@ def _test_output_attrs(calc, dtype_out): - with xr.open_dataset(calc.path_out[dtype_out]) as data: - expected_units = calc.var.units - if calc.dtype_out_vert == 'vert_int': - if expected_units != '': - expected_units = ("(vertical integral of {0}):" - " {0} m)").format(expected_units) - else: - expected_units = ("(vertical integral of quantity" - " with unspecified units)") - expected_description = calc.var.description - for name, arr in data.data_vars.items(): - assert expected_units == arr.attrs['units'] - assert expected_description == arr.attrs['description'] + with xr.set_options(enable_cftimeindex=True): + with xr.open_dataset(calc.path_out[dtype_out]) as data: + expected_units = calc.var.units + if calc.dtype_out_vert == 'vert_int': + if expected_units != '': + expected_units = ("(vertical integral of {0}):" + " {0} m)").format(expected_units) + else: + expected_units = ("(vertical integral of quantity" + " with unspecified units)") + expected_description = calc.var.description + for name, arr in data.data_vars.items(): + assert expected_units == arr.attrs['units'] + assert expected_description == arr.attrs['description'] def _test_files_and_attrs(calc, dtype_out): @@ -237,14 +238,16 @@ def test_recursive_calculation(recursive_test_params): calc = Calc(intvl_out='ann', dtype_out_time='av', **basic_params) calc = calc.compute() - expected = xr.open_dataset( - calc.path_out['av'], autoclose=True)['condensation_rain'] + with xr.set_options(enable_cftimeindex=True): + expected = xr.open_dataset( + calc.path_out['av'], autoclose=True)['condensation_rain'] _test_files_and_attrs(calc, 'av') calc = Calc(intvl_out='ann', dtype_out_time='av', **recursive_params) calc = calc.compute() - result = xr.open_dataset( - calc.path_out['av'], autoclose=True)['recursive_condensation_rain'] + with xr.set_options(enable_cftimeindex=True): + result = xr.open_dataset( + calc.path_out['av'], autoclose=True)['recursive_condensation_rain'] _test_files_and_attrs(calc, 'av') xr.testing.assert_equal(expected, result) From f941ebc071c965420e9814026d7790147b70842d Mon Sep 17 00:00:00 2001 From: Spencer Clark Date: Sat, 19 May 2018 08:51:05 -0400 Subject: [PATCH 03/17] Add tolerance to assert_has_data_for_time --- aospy/test/test_utils_times.py | 63 +++++++++++++++++++++++++++++++--- aospy/utils/times.py | 10 +++--- 2 files changed, 64 insertions(+), 9 deletions(-) diff --git a/aospy/test/test_utils_times.py b/aospy/test/test_utils_times.py index 79f5b72..61697ea 100755 --- a/aospy/test/test_utils_times.py +++ b/aospy/test/test_utils_times.py @@ -3,6 +3,7 @@ import datetime import warnings +import cftime import numpy as np import pandas as pd import pytest @@ -404,12 +405,66 @@ def test_assert_has_data_for_time(): ds = xr.decode_cf(ds) da = ds[var_name] - start_date = np.datetime64('2000-01-01', 'ns') - end_date = np.datetime64('2000-03-31', 'ns') + start_date = np.datetime64('2000-01-01') + end_date = np.datetime64('2000-03-31') + _assert_has_data_for_time(da, start_date, end_date) + + start_date_bad = np.datetime64('1999-12-31') + end_date_bad = np.datetime64('2000-04-01') + + with pytest.raises(AssertionError): + _assert_has_data_for_time(da, start_date_bad, end_date) + + with pytest.raises(AssertionError): + _assert_has_data_for_time(da, start_date, end_date_bad) + + with pytest.raises(AssertionError): + _assert_has_data_for_time(da, start_date_bad, end_date_bad) + + +CFTIME_DATE_TYPES = { + 'noleap': cftime.DatetimeNoLeap, + '365_day': cftime.DatetimeNoLeap, + '360_day': cftime.Datetime360Day, + 'julian': cftime.DatetimeJulian, + 'all_leap': cftime.DatetimeAllLeap, + '366_day': cftime.DatetimeAllLeap, + 'gregorian': cftime.DatetimeGregorian, + 'proleptic_gregorian': cftime.DatetimeProlepticGregorian +} + + +@pytest.mark.parametrize(['calendar', 'date_type'], + list(CFTIME_DATE_TYPES.items())) +def test_assert_has_data_for_time_cftime_datetimes(calendar, date_type): + time_bounds = np.array([[0, 2], [2, 4], [4, 6]]) + nv = np.array([0, 1]) + time = np.array([1, 3, 5]) + data = np.zeros((3)) + var_name = 'a' + ds = xr.DataArray(data, + coords=[time], + dims=[TIME_STR], + name=var_name).to_dataset() + ds[TIME_BOUNDS_STR] = xr.DataArray(time_bounds, + coords=[time, nv], + dims=[TIME_STR, BOUNDS_STR], + name=TIME_BOUNDS_STR) + units_str = 'days since 0002-01-02 00:00:00' + ds[TIME_STR].attrs['units'] = units_str + ds[TIME_STR].attrs['calendar'] = calendar + ds = ensure_time_avg_has_cf_metadata(ds) + ds = set_grid_attrs_as_coords(ds) + with xr.set_options(enable_cftimeindex=True): + ds = xr.decode_cf(ds) + da = ds[var_name] + + start_date = date_type(2, 1, 2) + end_date = date_type(2, 1, 8) _assert_has_data_for_time(da, start_date, end_date) - start_date_bad = np.datetime64('1999-12-31', 'ns') - end_date_bad = np.datetime64('2000-04-01', 'ns') + start_date_bad = date_type(2, 1, 1) + end_date_bad = date_type(2, 1, 9) with pytest.raises(AssertionError): _assert_has_data_for_time(da, start_date_bad, end_date) diff --git a/aospy/utils/times.py b/aospy/utils/times.py index e1170f0..eb40672 100644 --- a/aospy/utils/times.py +++ b/aospy/utils/times.py @@ -481,11 +481,11 @@ def _assert_has_data_for_time(da, start_date, end_date): da_start, da_end = times.values message = ('Data does not exist for requested time range: {0} to {1};' ' found data from time range: {2} to {3}.') - print(da_start) - print(da_end) - print(start_date) - print(end_date) - range_exists = da_start <= start_date and da_end >= end_date + # Add tolerance of one second, due to precision of cftime.datetimes + tol = datetime.timedelta(seconds=1) + if isinstance(da_start, np.datetime64): + tol = np.timedelta64(tol, 'ns') + range_exists = (da_start - tol) <= start_date and (da_end + tol) >= end_date assert (range_exists), message.format(start_date, end_date, da_start, da_end) From cbfe56a8d2ce0a004ff617c40bc4db890783afc4 Mon Sep 17 00:00:00 2001 From: Spencer Clark Date: Sat, 19 May 2018 09:03:54 -0400 Subject: [PATCH 04/17] Update and test ensure_datetime --- aospy/test/test_utils_times.py | 10 +++++----- aospy/utils/times.py | 3 +-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/aospy/test/test_utils_times.py b/aospy/test/test_utils_times.py index 61697ea..0607dbc 100755 --- a/aospy/test/test_utils_times.py +++ b/aospy/test/test_utils_times.py @@ -37,7 +37,7 @@ ) -_INVALID_DATE_OBJECTS = [1985, True, None, '2016-04-07', np.datetime64(1, 'Y')] +_INVALID_DATE_OBJECTS = [1985, True, None, '2016-04-07'] def test_apply_time_offset(): @@ -111,10 +111,10 @@ def test_monthly_mean_at_each_ind(): assert actual.identical(desired) -def test_ensure_datetime_valid_input(): - for date in [datetime.datetime(1981, 7, 15), - datetime.datetime(1, 1, 1)]: - assert ensure_datetime(date) == date +@pytest.mark.parametrize('date', [np.datetime64(2000, 1, 1), + cftime.DatetimeNoLeap(1, 1, 1)]) +def test_ensure_datetime_valid_input(date): + assert ensure_datetime(date) == date def test_ensure_datetime_invalid_input(): diff --git a/aospy/utils/times.py b/aospy/utils/times.py index eb40672..7e765b9 100644 --- a/aospy/utils/times.py +++ b/aospy/utils/times.py @@ -1,5 +1,4 @@ """Utility functions for handling times, dates, etc.""" -import datetime import warnings import cftime @@ -188,7 +187,7 @@ def ensure_datetime(obj): ------ TypeError if `obj` is not datetime-like """ - if isinstance(obj, (datetime.datetime, cftime.datetime, np.datetime64)): + if isinstance(obj, (cftime.datetime, np.datetime64)): return obj raise TypeError("datetime-like object required. " "Type given: {}".format(type(obj))) From a1e298cfc4df0f001389e3e165dc4c57ef5b712d Mon Sep 17 00:00:00 2001 From: Spencer Clark Date: Sat, 19 May 2018 10:43:35 -0400 Subject: [PATCH 05/17] Use np.datetime64 or cftime.datetime instead of datetime.datetime --- aospy/data_loader.py | 5 ++++- aospy/test/test_data_loader.py | 35 +++++++++++++++++++--------------- aospy/test/test_run.py | 16 ++++++++++------ aospy/test/test_utils_times.py | 28 ++++++++++++++++++++++++--- aospy/utils/times.py | 20 +++++++++++++++++++ 5 files changed, 79 insertions(+), 25 deletions(-) diff --git a/aospy/data_loader.py b/aospy/data_loader.py index df8a779..7dab247 100644 --- a/aospy/data_loader.py +++ b/aospy/data_loader.py @@ -603,9 +603,12 @@ def _input_data_paths_gfdl(self, name, start_date, end_date, domain, else: subdir = os.path.join(intvl_in, dur_str) direc = os.path.join(self.data_direc, domain, dtype_lbl, subdir) + data_start_date = times.maybe_cast_as_timestamp(self.data_start_date) + start_date = times.maybe_cast_as_timestamp(start_date) + end_date = times.maybe_cast_as_timestamp(end_date) files = [os.path.join(direc, io.data_name_gfdl( name, domain, dtype, intvl_in, year, intvl_out, - self.data_start_date.year, self.data_dur)) + data_start_date.year, self.data_dur)) for year in range(start_date.year, end_date.year + 1)] files = list(set(files)) files.sort() diff --git a/aospy/test/test_data_loader.py b/aospy/test/test_data_loader.py index fae482c..1ae72d6 100644 --- a/aospy/test/test_data_loader.py +++ b/aospy/test/test_data_loader.py @@ -1,6 +1,5 @@ #!/usr/bin/env python """Test suite for aospy.data_loader module.""" -from datetime import datetime import os import unittest import warnings @@ -45,8 +44,8 @@ class AospyDataLoaderTestCase(unittest.TestCase): def setUp(self): self.DataLoader = DataLoader() self.generate_file_set_args = dict( - var=condensation_rain, start_date=datetime(2000, 1, 1), - end_date=datetime(2002, 12, 31), domain='atmos', + var=condensation_rain, start_date=np.datetime64('2000-01-01'), + end_date=np.datetime64('2002-12-31'), domain='atmos', intvl_in='monthly', dtype_in_vert='sigma', dtype_in_time='ts', intvl_out=None) time_bounds = np.array([[0, 31], [31, 59], [59, 90]]) @@ -236,8 +235,8 @@ def setUp(self): self.DataLoader = GFDLDataLoader( data_direc=os.path.join('.', 'test'), data_dur=6, - data_start_date=datetime(2000, 1, 1), - data_end_date=datetime(2012, 12, 31), + data_start_date=np.datetime64('2000-01-01'), + data_end_date=np.datetime64('2012-12-31'), upcast_float32=False, data_vars='minimal', coords='minimal' @@ -258,12 +257,12 @@ def test_overriding_constructor(self): self.assertEqual(new.data_dur, 8) new = GFDLDataLoader(self.DataLoader, - data_start_date=datetime(2001, 1, 1)) - self.assertEqual(new.data_start_date, datetime(2001, 1, 1)) + data_start_date=np.datetime64('2001-01-01')) + self.assertEqual(new.data_start_date, np.datetime64('2001-01-01')) new = GFDLDataLoader(self.DataLoader, - data_end_date=datetime(2003, 12, 31)) - self.assertEqual(new.data_end_date, datetime(2003, 12, 31)) + data_end_date=np.datetime64('2003-12-31')) + self.assertEqual(new.data_end_date, np.datetime64('2003-12-31')) new = GFDLDataLoader(self.DataLoader, preprocess_func=lambda ds: ds) @@ -315,7 +314,8 @@ def test_input_data_paths_gfdl(self): expected = [os.path.join('.', 'test', 'atmos', 'ts', 'monthly', '6yr', 'atmos.200601-201112.temp.nc')] result = self.DataLoader._input_data_paths_gfdl( - 'temp', datetime(2010, 1, 1), datetime(2010, 12, 31), 'atmos', + 'temp', np.datetime64('2010-01-01'), + np.datetime64('2010-12-31'), 'atmos', 'monthly', 'pressure', 'ts', None) self.assertEqual(result, expected) @@ -323,35 +323,40 @@ def test_input_data_paths_gfdl(self): '6yr', 'atmos_daily.20060101-20111231.temp.nc')] result = self.DataLoader._input_data_paths_gfdl( - 'temp', datetime(2010, 1, 1), datetime(2010, 12, 31), 'atmos', + 'temp', np.datetime64('2010-01-01'), + np.datetime64('2010-12-31'), 'atmos', 'daily', 'pressure', 'ts', None) self.assertEqual(result, expected) expected = [os.path.join('.', 'test', 'atmos_level', 'ts', 'monthly', '6yr', 'atmos_level.200601-201112.temp.nc')] result = self.DataLoader._input_data_paths_gfdl( - 'temp', datetime(2010, 1, 1), datetime(2010, 12, 31), 'atmos', + 'temp', np.datetime64('2010-01-01'), + np.datetime64('2010-12-31'), 'atmos', 'monthly', ETA_STR, 'ts', None) self.assertEqual(result, expected) expected = [os.path.join('.', 'test', 'atmos', 'ts', 'monthly', '6yr', 'atmos.200601-201112.ps.nc')] result = self.DataLoader._input_data_paths_gfdl( - 'ps', datetime(2010, 1, 1), datetime(2010, 12, 31), 'atmos', + 'ps', np.datetime64('2010-01-01'), + np.datetime64('2010-12-31'), 'atmos', 'monthly', ETA_STR, 'ts', None) self.assertEqual(result, expected) expected = [os.path.join('.', 'test', 'atmos_inst', 'ts', 'monthly', '6yr', 'atmos_inst.200601-201112.temp.nc')] result = self.DataLoader._input_data_paths_gfdl( - 'temp', datetime(2010, 1, 1), datetime(2010, 12, 31), 'atmos', + 'temp', np.datetime64('2010-01-01'), + np.datetime64('2010-12-31'), 'atmos', 'monthly', 'pressure', 'inst', None) self.assertEqual(result, expected) expected = [os.path.join('.', 'test', 'atmos', 'av', 'monthly_6yr', 'atmos.2006-2011.jja.nc')] result = self.DataLoader._input_data_paths_gfdl( - 'temp', datetime(2010, 1, 1), datetime(2010, 12, 31), 'atmos', + 'temp', np.datetime64('2010-01-01'), + np.datetime64('2010-12-31'), 'atmos', 'monthly', 'pressure', 'av', 'jja') self.assertEqual(result, expected) diff --git a/aospy/test/test_run.py b/aospy/test/test_run.py index 96fa9be..2ef8c37 100644 --- a/aospy/test/test_run.py +++ b/aospy/test/test_run.py @@ -1,9 +1,11 @@ #!/usr/bin/env python """Test suite for aospy.run module.""" -import datetime import sys import unittest +import cftime +import numpy as np + from aospy.run import Run from aospy.data_loader import DictDataLoader, GFDLDataLoader @@ -19,7 +21,7 @@ def tearDown(self): class TestRun(RunTestCase): def test_init_dates_valid_input(self): for attr in ['default_start_date', 'default_end_date']: - for date in [None, datetime.datetime(1, 1, 1)]: + for date in [None, np.datetime64('2000-01-01')]: run_ = Run(**{attr: date}) self.assertEqual(date, getattr(run_, attr)) @@ -30,11 +32,13 @@ def test_init_dates_invalid_input(self): Run(**{attr: date}) def test_init_default_dates(self): - gdl = GFDLDataLoader(data_start_date=datetime.datetime(1, 1, 1), - data_end_date=datetime.datetime(1, 12, 31)) + gdl = GFDLDataLoader(data_start_date=cftime.DatetimeNoLeap(1, 1, 1), + data_end_date=cftime.DatetimeNoLeap(1, 12, 31)) run_ = Run(data_loader=gdl) - self.assertEqual(run_.default_start_date, datetime.datetime(1, 1, 1)) - self.assertEqual(run_.default_end_date, datetime.datetime(1, 12, 31)) + self.assertEqual(run_.default_start_date, + cftime.DatetimeNoLeap(1, 1, 1)) + self.assertEqual(run_.default_end_date, + cftime.DatetimeNoLeap(1, 12, 31)) ddl = DictDataLoader({'monthly': '/a/'}) run_ = Run(data_loader=ddl) diff --git a/aospy/test/test_utils_times.py b/aospy/test/test_utils_times.py index 0607dbc..b1532b0 100755 --- a/aospy/test/test_utils_times.py +++ b/aospy/test/test_utils_times.py @@ -33,7 +33,8 @@ assert_matching_time_coord, ensure_time_as_index, sel_time, - yearly_average + yearly_average, + maybe_cast_as_timestamp ) @@ -111,7 +112,7 @@ def test_monthly_mean_at_each_ind(): assert actual.identical(desired) -@pytest.mark.parametrize('date', [np.datetime64(2000, 1, 1), +@pytest.mark.parametrize('date', [np.datetime64('2000-01-01'), cftime.DatetimeNoLeap(1, 1, 1)]) def test_ensure_datetime_valid_input(date): assert ensure_datetime(date) == date @@ -124,7 +125,7 @@ def test_ensure_datetime_invalid_input(): def test_datetime_or_default(): - date = datetime.datetime(1, 2, 3) + date = np.datetime64('2000-01-01') assert datetime_or_default(None, 'dummy') == 'dummy' assert datetime_or_default(date, 'dummy') == ensure_datetime(date) @@ -610,3 +611,24 @@ def test_average_time_bounds(ds_time_encoded_cf): coords={TIME_STR: desired_values}, name=TIME_STR) xr.testing.assert_identical(actual, desired) + + +def test_maybe_cast_as_timestamp_datetime64(): + date = np.datetime64('2000-01-01') + expected = pd.Timestamp(date) + result = maybe_cast_as_timestamp(date) + assert result == expected + + +def test_maybe_cast_as_timestamp_string(): + date = '2000-01-01' + expected = pd.Timestamp(date) + result = maybe_cast_as_timestamp(date) + assert result == expected + + +def test_maybe_cast_as_timestamp_cftime_datetime(): + date = cftime.DatetimeNoLeap(2000, 1, 1) + expected = date + result = maybe_cast_as_timestamp(date) + assert result == expected diff --git a/aospy/utils/times.py b/aospy/utils/times.py index 7e765b9..6e22b80 100644 --- a/aospy/utils/times.py +++ b/aospy/utils/times.py @@ -1,4 +1,5 @@ """Utility functions for handling times, dates, etc.""" +import datetime import warnings import cftime @@ -575,3 +576,22 @@ def ensure_time_as_index(ds): da[TIME_STR] = ds[TIME_STR] ds[name] = da return ds + + +def maybe_cast_as_timestamp(date): + """Convert a date to a pd.Timestamp object if possible + + Parameters + ---------- + date : datetime-like object + Input datetime + + Returns + ------- + pd.Timestamp or cftime.datetime + """ + try: + return pd.to_datetime(date) + except TypeError: + return date + From 6a704a6372a5c9b092e9dd0ff6285d3455c10e07 Mon Sep 17 00:00:00 2001 From: Spencer Clark Date: Sat, 19 May 2018 10:53:10 -0400 Subject: [PATCH 06/17] Remove workaround logic --- aospy/test/test_utils_times.py | 111 --------------------------------- aospy/utils/times.py | 79 ----------------------- 2 files changed, 190 deletions(-) diff --git a/aospy/test/test_utils_times.py b/aospy/test/test_utils_times.py index b1532b0..7c507f7 100755 --- a/aospy/test/test_utils_times.py +++ b/aospy/test/test_utils_times.py @@ -1,7 +1,6 @@ #!/usr/bin/env python """Test suite for aospy.timedate module.""" import datetime -import warnings import cftime import numpy as np @@ -22,8 +21,6 @@ monthly_mean_at_each_ind, ensure_datetime, datetime_or_default, - numpy_datetime_range_workaround, - numpy_datetime_workaround_encode_cf, month_indices, _month_conditional, extract_months, @@ -130,114 +127,6 @@ def test_datetime_or_default(): assert datetime_or_default(date, 'dummy') == ensure_datetime(date) -def test_numpy_datetime_range_workaround(): - assert (numpy_datetime_range_workaround( - datetime.datetime(pd.Timestamp.min.year + 1, 1, 1), - pd.Timestamp.min.year + 1, pd.Timestamp.min.year + 2) == - datetime.datetime(pd.Timestamp.min.year + 1, 1, 1)) - - assert ( - numpy_datetime_range_workaround(datetime.datetime(3, 1, 1), 1, 6) == - datetime.datetime(pd.Timestamp.min.year + 3, 1, 1) - ) - - assert ( - numpy_datetime_range_workaround(datetime.datetime(5, 1, 1), 4, 6) == - datetime.datetime(pd.Timestamp.min.year + 2, 1, 1) - ) - - # Test min_yr outside valid range - assert ( - numpy_datetime_range_workaround( - datetime.datetime(pd.Timestamp.min.year + 3, 1, 1), - pd.Timestamp.min.year, pd.Timestamp.min.year + 2) == - datetime.datetime(pd.Timestamp.min.year + 4, 1, 1) - ) - - # Test max_yr outside valid range - assert ( - numpy_datetime_range_workaround( - datetime.datetime(pd.Timestamp.max.year + 2, 1, 1), - pd.Timestamp.max.year - 1, pd.Timestamp.max.year) == - datetime.datetime(pd.Timestamp.min.year + 4, 1, 1)) - - -def _create_datetime_workaround_test_data(days, ref_units, expected_units): - # 1095 days corresponds to three years in a noleap calendar - # This allows us to generate ranges which straddle the - # Timestamp-valid range - three_yrs = 1095. - time = xr.DataArray([days, days + three_yrs], dims=[TIME_STR]) - ds = xr.Dataset(coords={TIME_STR: time}) - ds[TIME_STR].attrs['units'] = ref_units - ds[TIME_STR].attrs['calendar'] = 'noleap' - actual, min_yr, max_yr = numpy_datetime_workaround_encode_cf(ds) - - time_desired = xr.DataArray([days, days + three_yrs], - dims=[TIME_STR]) - desired = xr.Dataset(coords={TIME_STR: time_desired}) - desired[TIME_STR].attrs['units'] = expected_units - desired[TIME_STR].attrs['calendar'] = 'noleap' - return actual, min_yr, max_yr, desired - - -def _numpy_datetime_workaround_encode_cf_tests(days, ref_units, expected_units, - expected_time0, expected_min_yr, - expected_max_yr): - with warnings.catch_warnings(record=True) as warnlog: - actual, minyr, maxyr, desired = _create_datetime_workaround_test_data( - days, ref_units, expected_units) - assert len(warnlog) == 0 - xr.testing.assert_identical(actual, desired) - assert xr.decode_cf(actual).time.values[0] == expected_time0 - assert minyr == expected_min_yr - assert maxyr == expected_max_yr - - -def test_numpy_datetime_workaround_encode_cf(): - # 255169 days from 0001-01-01 corresponds to date 700-02-04. - _numpy_datetime_workaround_encode_cf_tests( - 255169., 'days since 0001-01-01 00:00:00', - 'days since 979-01-01 00:00:00', np.datetime64('1678-02-04'), - 700, 703) - - # Test a case where times are in the Timestamp-valid range - _numpy_datetime_workaround_encode_cf_tests( - 10., 'days since 2000-01-01 00:00:00', - 'days since 2000-01-01 00:00:00', np.datetime64('2000-01-11'), - 2000, 2003) - - # Regression tests for GH188 - _numpy_datetime_workaround_encode_cf_tests( - 732., 'days since 0700-01-01 00:00:00', - 'days since 1676-01-01 00:00:00', np.datetime64('1678-01-03'), - 702, 705) - - # Non-January 1st reference date - _numpy_datetime_workaround_encode_cf_tests( - 732., 'days since 0700-05-03 00:00:00', - 'days since 1676-05-03 00:00:00', np.datetime64('1678-05-05'), - 702, 705) - - # Above Timestamp.max - _numpy_datetime_workaround_encode_cf_tests( - 732., 'days since 2300-01-01 00:00:00', - 'days since 1676-01-01 00:00:00', np.datetime64('1678-01-03'), - 2302, 2305) - - # Straddle lower bound - _numpy_datetime_workaround_encode_cf_tests( - 2., 'days since 1677-01-01 00:00:00', - 'days since 1678-01-01 00:00:00', np.datetime64('1678-01-03'), - 1677, 1680) - - # Straddle upper bound - _numpy_datetime_workaround_encode_cf_tests( - 2., 'days since 2262-01-01 00:00:00', - 'days since 1678-01-01 00:00:00', np.datetime64('1678-01-03'), - 2262, 2265) - - def test_month_indices(): np.testing.assert_array_equal(month_indices('ann'), range(1, 13)) np.testing.assert_array_equal(month_indices('jja'), diff --git a/aospy/utils/times.py b/aospy/utils/times.py index 6e22b80..a187de4 100644 --- a/aospy/utils/times.py +++ b/aospy/utils/times.py @@ -214,85 +214,6 @@ def datetime_or_default(date, default): return ensure_datetime(date) -def numpy_datetime_range_workaround(date, min_year, max_year): - """Reset a date to earliest allowable year if outside of valid range. - - Hack to address np.datetime64, and therefore pandas and xarray, not - supporting dates outside the range 1677-09-21 and 2262-04-11 due to - nanosecond precision. See e.g. - https://github.com/spencerahill/aospy/issues/96. - - - Parameters - ---------- - date : datetime.datetime object - min_year : int - Minimum year in the raw decoded dataset - max_year : int - Maximum year in the raw decoded dataset - - Returns - ------- - datetime.datetime object - Original datetime.datetime object if the original date is within the - permissible dates, otherwise a datetime.datetime object with the year - offset to the earliest allowable year. - """ - min_yr_in_range = pd.Timestamp.min.year < min_year < pd.Timestamp.max.year - max_yr_in_range = pd.Timestamp.min.year < max_year < pd.Timestamp.max.year - if not (min_yr_in_range and max_yr_in_range): - return datetime.datetime( - date.year - min_year + pd.Timestamp.min.year + 1, - date.month, date.day) - return date - - -def numpy_datetime_workaround_encode_cf(ds): - """Generate CF-compliant units for out-of-range dates. - - Hack to address np.datetime64, and therefore pandas and xarray, not - supporting dates outside the range 1677-09-21 and 2262-04-11 due to - nanosecond precision. See e.g. - https://github.com/spencerahill/aospy/issues/96. - - Specifically, we coerce the data such that, when decoded, the earliest - value starts in 1678 but with its month, day, and shorter timescales - (hours, minutes, seconds, etc.) intact and with the time-spacing between - values intact. - - Parameters - ---------- - ds : xarray.Dataset - - Returns - ------- - xarray.Dataset, int, int - Dataset with time units adjusted as needed, minimum year - in loaded data, and maximum year in loaded data. - """ - time = ds[TIME_STR] - units = time.attrs['units'] - units_yr = units.split(' since ')[1].split('-')[0] - with warnings.catch_warnings(record=True): - min_yr_decoded = xr.decode_cf(time.to_dataset(name='dummy')) - min_date = min_yr_decoded[TIME_STR].values[0] - max_date = min_yr_decoded[TIME_STR].values[-1] - if all(isinstance(date, np.datetime64) for date in [min_date, max_date]): - return ds, pd.Timestamp(min_date).year, pd.Timestamp(max_date).year - else: - min_yr = min_date.year - max_yr = max_date.year - offset = int(units_yr) - min_yr + 1 - new_units_yr = pd.Timestamp.min.year + offset - new_units = units.replace(units_yr, str(new_units_yr)) - - for VAR_STR in TIME_VAR_STRS: - if VAR_STR in ds: - var = ds[VAR_STR] - var.attrs['units'] = new_units - return ds, min_yr, max_yr - - def month_indices(months): """Convert string labels for months to integer indices. From 58d5e2481f31e5967db64dc6853b087990bc2cec Mon Sep 17 00:00:00 2001 From: Spencer Clark Date: Sat, 19 May 2018 11:51:12 -0400 Subject: [PATCH 07/17] Switch to Win-64 from Win-32 platform for Python 2.7 on Appveyor --- appveyor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 5ee68d9..abe3b8a 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,7 +7,7 @@ environment: matrix: - PYTHON: "C:\\Python27-conda32" PYTHON_VERSION: "2.7" - PYTHON_ARCH: "32" + PYTHON_ARCH: "64" CONDA_ENV: "py27" - PYTHON: "C:\\Python35-conda64" @@ -41,4 +41,4 @@ install: build: false test_script: - - "py.test aospy --verbose" \ No newline at end of file + - "py.test aospy --verbose" From 55ad48cfd1d36b65cf16459d19c0d7ff911db04b Mon Sep 17 00:00:00 2001 From: Spencer Clark Date: Sat, 19 May 2018 11:52:25 -0400 Subject: [PATCH 08/17] Typo --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index abe3b8a..b377849 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,7 +5,7 @@ environment: matrix: - - PYTHON: "C:\\Python27-conda32" + - PYTHON: "C:\\Python27-conda64" PYTHON_VERSION: "2.7" PYTHON_ARCH: "64" CONDA_ENV: "py27" From 45304f79e6a2f05d0c7efdf05b3255be81d33418 Mon Sep 17 00:00:00 2001 From: Spencer Clark Date: Sun, 20 May 2018 09:43:12 -0400 Subject: [PATCH 09/17] Support all types of datetime range input aospy will now automatically convert user-input date ranges to the types required for indexing the data read in. Accepted types are: - np.datetime64 - datetime.datetime - cftime.datetime --- aospy/data_loader.py | 6 +- aospy/examples/example_obj_lib.py | 7 +- aospy/examples/tutorial.ipynb | 291 ++++++++++++------------------ aospy/test/test_calc_basic.py | 5 +- aospy/test/test_data_loader.py | 42 ++++- aospy/test/test_utils_times.py | 37 +++- aospy/utils/times.py | 40 +++- 7 files changed, 233 insertions(+), 195 deletions(-) diff --git a/aospy/data_loader.py b/aospy/data_loader.py index 7dab247..51e5e99 100644 --- a/aospy/data_loader.py +++ b/aospy/data_loader.py @@ -278,12 +278,14 @@ def load_variable(self, var=None, start_date=None, end_date=None, coords=self.coords, start_date=start_date, end_date=end_date, time_offset=time_offset, **DataAttrs ) - ds = _prep_time_data(ds) + start_date = times.maybe_convert_to_index_date_type( + ds.indexes[TIME_STR], start_date) + end_date = times.maybe_convert_to_index_date_type( + ds.indexes[TIME_STR], end_date) ds = set_grid_attrs_as_coords(ds) da = _sel_var(ds, var, self.upcast_float32) da = self._maybe_apply_time_shift(da, time_offset, **DataAttrs) - return times.sel_time(da, start_date, end_date).load() def recursively_compute_variable(self, var, start_date=None, end_date=None, diff --git a/aospy/examples/example_obj_lib.py b/aospy/examples/example_obj_lib.py index 8387a45..91d3f6d 100644 --- a/aospy/examples/example_obj_lib.py +++ b/aospy/examples/example_obj_lib.py @@ -1,8 +1,7 @@ """Sample aospy object library using the included example data.""" +from datetime import datetime import os -from cftime import DatetimeNoLeap - import aospy from aospy import Model, Proj, Region, Run, Var from aospy.data_loader import DictDataLoader @@ -18,8 +17,8 @@ description=( 'Control simulation of the idealized moist model' ), - default_start_date=DatetimeNoLeap(4, 1, 1), - default_end_date=DatetimeNoLeap(6, 12, 31), + default_start_date=datetime(4, 1, 1), + default_end_date=datetime(6, 12, 31), data_loader=DictDataLoader(_file_map) ) diff --git a/aospy/examples/tutorial.ipynb b/aospy/examples/tutorial.ipynb index 10535e4..c703873 100644 --- a/aospy/examples/tutorial.ipynb +++ b/aospy/examples/tutorial.ipynb @@ -407,106 +407,46 @@ "name": "stderr", "output_type": "stream", "text": [ - "INFO:root:Getting input data: Var instance \"precip_largescale\" (Sat May 19 07:19:45 2018)\n", + "INFO:root:Getting input data: Var instance \"precip_largescale\" (Sun May 20 09:40:28 2018)\n", "WARNING:root:Datapoints were stored using the np.float32 datatype.For accurate reduction operations using bottleneck, datapoints are being cast to the np.float64 datatype. For more information see: https://github.com/pydata/xarray/issues/1346\n", - "WARNING:root:Skipping aospy calculation `` due to error with the following traceback: \n", - "Traceback (most recent call last):\n", - " File \"/Users/spencerclark/aospy/aospy/automate.py\", line 270, in _compute_or_skip_on_error\n", - " return calc.compute(**compute_kwargs)\n", - " File \"/Users/spencerclark/aospy/aospy/calc.py\", line 569, in compute\n", - " self.end_date),\n", - " File \"/Users/spencerclark/aospy/aospy/calc.py\", line 415, in _get_all_data\n", - " for n, var in enumerate(self.variables)]\n", - " File \"/Users/spencerclark/aospy/aospy/calc.py\", line 415, in \n", - " for n, var in enumerate(self.variables)]\n", - " File \"/Users/spencerclark/aospy/aospy/calc.py\", line 367, in _get_input_data\n", - " **self.data_loader_attrs)\n", - " File \"/Users/spencerclark/aospy/aospy/data_loader.py\", line 318, in recursively_compute_variable\n", - " **DataAttrs)\n", - " File \"/Users/spencerclark/aospy/aospy/data_loader.py\", line 287, in load_variable\n", - " return times.sel_time(da, start_date, end_date).load()\n", - " File \"/Users/spencerclark/aospy/aospy/utils/times.py\", line 515, in sel_time\n", - " _assert_has_data_for_time(da, start_date, end_date)\n", - " File \"/Users/spencerclark/aospy/aospy/utils/times.py\", line 484, in _assert_has_data_for_time\n", - " range_exists = start_date >= da_start and end_date <= da_end\n", - " File \"cftime/_cftime.pyx\", line 1615, in cftime._cftime.datetime.__richcmp__\n", - "TypeError: cannot compare cftime.DatetimeNoLeap(4, 1, 1, 0, 0, 0, 0, 6, 1) and datetime.datetime(4, 1, 1, 0, 0) (different calendars)\n", - "\n", - "INFO:root:Getting input data: Var instance \"precip_convective\" (Sat May 19 07:19:45 2018)\n", + "INFO:root:Getting input data: Var instance \"precip_convective\" (Sun May 20 09:40:28 2018)\n", "WARNING:root:Datapoints were stored using the np.float32 datatype.For accurate reduction operations using bottleneck, datapoints are being cast to the np.float64 datatype. For more information see: https://github.com/pydata/xarray/issues/1346\n", - "WARNING:root:Skipping aospy calculation `` due to error with the following traceback: \n", - "Traceback (most recent call last):\n", - " File \"/Users/spencerclark/aospy/aospy/automate.py\", line 270, in _compute_or_skip_on_error\n", - " return calc.compute(**compute_kwargs)\n", - " File \"/Users/spencerclark/aospy/aospy/calc.py\", line 569, in compute\n", - " self.end_date),\n", - " File \"/Users/spencerclark/aospy/aospy/calc.py\", line 415, in _get_all_data\n", - " for n, var in enumerate(self.variables)]\n", - " File \"/Users/spencerclark/aospy/aospy/calc.py\", line 415, in \n", - " for n, var in enumerate(self.variables)]\n", - " File \"/Users/spencerclark/aospy/aospy/calc.py\", line 367, in _get_input_data\n", - " **self.data_loader_attrs)\n", - " File \"/Users/spencerclark/aospy/aospy/data_loader.py\", line 318, in recursively_compute_variable\n", - " **DataAttrs)\n", - " File \"/Users/spencerclark/aospy/aospy/data_loader.py\", line 287, in load_variable\n", - " return times.sel_time(da, start_date, end_date).load()\n", - " File \"/Users/spencerclark/aospy/aospy/utils/times.py\", line 515, in sel_time\n", - " _assert_has_data_for_time(da, start_date, end_date)\n", - " File \"/Users/spencerclark/aospy/aospy/utils/times.py\", line 484, in _assert_has_data_for_time\n", - " range_exists = start_date >= da_start and end_date <= da_end\n", - " File \"cftime/_cftime.pyx\", line 1615, in cftime._cftime.datetime.__richcmp__\n", - "TypeError: cannot compare cftime.DatetimeNoLeap(4, 1, 1, 0, 0, 0, 0, 6, 1) and datetime.datetime(4, 1, 1, 0, 0) (different calendars)\n", - "\n", - "INFO:root:Getting input data: Var instance \"precip_largescale\" (Sat May 19 07:19:45 2018)\n", + "INFO:root:Computing timeseries for 0004-01-01 00:00:00 -- 0006-12-31 00:00:00.\n", + "INFO:root:Applying desired time-reduction methods. (Sun May 20 09:40:29 2018)\n", + "INFO:root:Writing desired gridded outputs to disk.\n", + "INFO:root:\texample-output/example_proj/example_model/example_run/precip_total/precip_total.ann.av.from_monthly_ts.example_model.example_run.0004-0006.nc\n", + "//anaconda/envs/aospy_dev/lib/python3.6/_collections_abc.py:743: FutureWarning: iteration over an xarray.Dataset will change in xarray v0.11 to only include data variables, not coordinates. Iterate over the Dataset.variables property instead to preserve existing behavior in a forwards compatible manner.\n", + " for key in self._mapping:\n", + "INFO:root:\texample-output/example_proj/example_model/example_run/precip_total/precip_total.ann.reg.av.from_monthly_ts.example_model.example_run.0004-0006.nc\n", + "INFO:root:Getting input data: Var instance \"precip_largescale\" (Sun May 20 09:40:29 2018)\n", "WARNING:root:Datapoints were stored using the np.float32 datatype.For accurate reduction operations using bottleneck, datapoints are being cast to the np.float64 datatype. For more information see: https://github.com/pydata/xarray/issues/1346\n", - "WARNING:root:Skipping aospy calculation `` due to error with the following traceback: \n", - "Traceback (most recent call last):\n", - " File \"/Users/spencerclark/aospy/aospy/automate.py\", line 270, in _compute_or_skip_on_error\n", - " return calc.compute(**compute_kwargs)\n", - " File \"/Users/spencerclark/aospy/aospy/calc.py\", line 569, in compute\n", - " self.end_date),\n", - " File \"/Users/spencerclark/aospy/aospy/calc.py\", line 415, in _get_all_data\n", - " for n, var in enumerate(self.variables)]\n", - " File \"/Users/spencerclark/aospy/aospy/calc.py\", line 415, in \n", - " for n, var in enumerate(self.variables)]\n", - " File \"/Users/spencerclark/aospy/aospy/calc.py\", line 367, in _get_input_data\n", - " **self.data_loader_attrs)\n", - " File \"/Users/spencerclark/aospy/aospy/data_loader.py\", line 318, in recursively_compute_variable\n", - " **DataAttrs)\n", - " File \"/Users/spencerclark/aospy/aospy/data_loader.py\", line 287, in load_variable\n", - " return times.sel_time(da, start_date, end_date).load()\n", - " File \"/Users/spencerclark/aospy/aospy/utils/times.py\", line 515, in sel_time\n", - " _assert_has_data_for_time(da, start_date, end_date)\n", - " File \"/Users/spencerclark/aospy/aospy/utils/times.py\", line 484, in _assert_has_data_for_time\n", - " range_exists = start_date >= da_start and end_date <= da_end\n", - " File \"cftime/_cftime.pyx\", line 1615, in cftime._cftime.datetime.__richcmp__\n", - "TypeError: cannot compare cftime.DatetimeNoLeap(4, 1, 1, 0, 0, 0, 0, 6, 1) and datetime.datetime(4, 1, 1, 0, 0) (different calendars)\n", - "\n", - "INFO:root:Getting input data: Var instance \"precip_largescale\" (Sat May 19 07:19:45 2018)\n", + "INFO:root:Getting input data: Var instance \"precip_convective\" (Sun May 20 09:40:29 2018)\n", "WARNING:root:Datapoints were stored using the np.float32 datatype.For accurate reduction operations using bottleneck, datapoints are being cast to the np.float64 datatype. For more information see: https://github.com/pydata/xarray/issues/1346\n", - "WARNING:root:Skipping aospy calculation `` due to error with the following traceback: \n", - "Traceback (most recent call last):\n", - " File \"/Users/spencerclark/aospy/aospy/automate.py\", line 270, in _compute_or_skip_on_error\n", - " return calc.compute(**compute_kwargs)\n", - " File \"/Users/spencerclark/aospy/aospy/calc.py\", line 569, in compute\n", - " self.end_date),\n", - " File \"/Users/spencerclark/aospy/aospy/calc.py\", line 415, in _get_all_data\n", - " for n, var in enumerate(self.variables)]\n", - " File \"/Users/spencerclark/aospy/aospy/calc.py\", line 415, in \n", - " for n, var in enumerate(self.variables)]\n", - " File \"/Users/spencerclark/aospy/aospy/calc.py\", line 367, in _get_input_data\n", - " **self.data_loader_attrs)\n", - " File \"/Users/spencerclark/aospy/aospy/data_loader.py\", line 318, in recursively_compute_variable\n", - " **DataAttrs)\n", - " File \"/Users/spencerclark/aospy/aospy/data_loader.py\", line 287, in load_variable\n", - " return times.sel_time(da, start_date, end_date).load()\n", - " File \"/Users/spencerclark/aospy/aospy/utils/times.py\", line 515, in sel_time\n", - " _assert_has_data_for_time(da, start_date, end_date)\n", - " File \"/Users/spencerclark/aospy/aospy/utils/times.py\", line 484, in _assert_has_data_for_time\n", - " range_exists = start_date >= da_start and end_date <= da_end\n", - " File \"cftime/_cftime.pyx\", line 1615, in cftime._cftime.datetime.__richcmp__\n", - "TypeError: cannot compare cftime.DatetimeNoLeap(4, 1, 1, 0, 0, 0, 0, 6, 1) and datetime.datetime(4, 1, 1, 0, 0) (different calendars)\n", - "\n" + "INFO:root:Computing timeseries for 0004-01-01 00:00:00 -- 0006-12-31 00:00:00.\n", + "INFO:root:Applying desired time-reduction methods. (Sun May 20 09:40:29 2018)\n", + "INFO:root:Writing desired gridded outputs to disk.\n", + "INFO:root:\texample-output/example_proj/example_model/example_run/precip_conv_frac/precip_conv_frac.ann.av.from_monthly_ts.example_model.example_run.0004-0006.nc\n", + "//anaconda/envs/aospy_dev/lib/python3.6/_collections_abc.py:743: FutureWarning: iteration over an xarray.Dataset will change in xarray v0.11 to only include data variables, not coordinates. Iterate over the Dataset.variables property instead to preserve existing behavior in a forwards compatible manner.\n", + " for key in self._mapping:\n", + "INFO:root:\texample-output/example_proj/example_model/example_run/precip_conv_frac/precip_conv_frac.ann.reg.av.from_monthly_ts.example_model.example_run.0004-0006.nc\n", + "INFO:root:Getting input data: Var instance \"precip_largescale\" (Sun May 20 09:40:29 2018)\n", + "WARNING:root:Datapoints were stored using the np.float32 datatype.For accurate reduction operations using bottleneck, datapoints are being cast to the np.float64 datatype. For more information see: https://github.com/pydata/xarray/issues/1346\n", + "INFO:root:Computing timeseries for 0004-01-01 00:00:00 -- 0006-12-31 00:00:00.\n", + "INFO:root:Applying desired time-reduction methods. (Sun May 20 09:40:29 2018)\n", + "INFO:root:Writing desired gridded outputs to disk.\n", + "INFO:root:\texample-output/example_proj/example_model/example_run/precip_largescale/precip_largescale.ann.av.from_monthly_ts.example_model.example_run.0004-0006.nc\n", + "//anaconda/envs/aospy_dev/lib/python3.6/_collections_abc.py:743: FutureWarning: iteration over an xarray.Dataset will change in xarray v0.11 to only include data variables, not coordinates. Iterate over the Dataset.variables property instead to preserve existing behavior in a forwards compatible manner.\n", + " for key in self._mapping:\n", + "INFO:root:\texample-output/example_proj/example_model/example_run/precip_largescale/precip_largescale.ann.reg.av.from_monthly_ts.example_model.example_run.0004-0006.nc\n", + "INFO:root:Getting input data: Var instance \"precip_convective\" (Sun May 20 09:40:29 2018)\n", + "WARNING:root:Datapoints were stored using the np.float32 datatype.For accurate reduction operations using bottleneck, datapoints are being cast to the np.float64 datatype. For more information see: https://github.com/pydata/xarray/issues/1346\n", + "INFO:root:Computing timeseries for 0004-01-01 00:00:00 -- 0006-12-31 00:00:00.\n", + "INFO:root:Applying desired time-reduction methods. (Sun May 20 09:40:30 2018)\n", + "INFO:root:Writing desired gridded outputs to disk.\n", + "INFO:root:\texample-output/example_proj/example_model/example_run/precip_convective/precip_convective.ann.av.from_monthly_ts.example_model.example_run.0004-0006.nc\n", + "//anaconda/envs/aospy_dev/lib/python3.6/_collections_abc.py:743: FutureWarning: iteration over an xarray.Dataset will change in xarray v0.11 to only include data variables, not coordinates. Iterate over the Dataset.variables property instead to preserve existing behavior in a forwards compatible manner.\n", + " for key in self._mapping:\n", + "INFO:root:\texample-output/example_proj/example_model/example_run/precip_convective/precip_convective.ann.reg.av.from_monthly_ts.example_model.example_run.0004-0006.nc\n" ] } ], @@ -542,10 +482,10 @@ { "data": { "text/plain": [ - "[Calc object: precip_conv_frac, example_proj, example_model, example_run,\n", - " Calc object: precip_total, example_proj, example_model, example_run,\n", - " Calc object: precip_convective, example_proj, example_model, example_run,\n", - " Calc object: precip_largescale, example_proj, example_model, example_run]" + "[,\n", + " ,\n", + " ,\n", + " ]" ] }, "execution_count": 13, @@ -572,8 +512,8 @@ { "data": { "text/plain": [ - "{'av': 'example-output/example_proj/example_model/example_run/precip_conv_frac/precip_conv_frac.ann.av.from_monthly_ts.example_model.example_run.0004-0006.nc',\n", - " 'reg.av': 'example-output/example_proj/example_model/example_run/precip_conv_frac/precip_conv_frac.ann.reg.av.from_monthly_ts.example_model.example_run.0004-0006.nc'}" + "{'av': 'example-output/example_proj/example_model/example_run/precip_total/precip_total.ann.av.from_monthly_ts.example_model.example_run.0004-0006.nc',\n", + " 'reg.av': 'example-output/example_proj/example_model/example_run/precip_total/precip_total.ann.reg.av.from_monthly_ts.example_model.example_run.0004-0006.nc'}" ] }, "execution_count": 14, @@ -601,42 +541,42 @@ "data": { "text/plain": [ "{'av': \n", - " array([[ 5.825489e-11, 5.568656e-06, 3.746100e-05, ..., 0.000000e+00,\n", - " 1.773260e-11, 2.142449e-11],\n", - " [ 1.718738e-03, 1.864318e-03, 1.876380e-03, ..., 1.584265e-03,\n", - " 1.650416e-03, 1.634984e-03],\n", - " [ 4.336546e-03, 3.980294e-03, 3.332168e-03, ..., 5.023761e-03,\n", - " 4.934178e-03, 4.846593e-03],\n", + " array([[ 5.158033e-06, 5.146671e-06, 5.135556e-06, ..., 5.176134e-06,\n", + " 5.170886e-06, 5.165684e-06],\n", + " [ 4.978642e-06, 5.003572e-06, 5.029685e-06, ..., 4.947511e-06,\n", + " 4.960262e-06, 4.970894e-06],\n", + " [ 5.608857e-06, 5.626041e-06, 5.659296e-06, ..., 5.449484e-06,\n", + " 5.514129e-06, 5.578681e-06],\n", " ..., \n", - " [ 4.826577e-03, 5.116871e-03, 5.306013e-03, ..., 3.952671e-03,\n", - " 3.857220e-03, 4.159514e-03],\n", - " [ 3.559006e-04, 4.071099e-04, 4.688400e-04, ..., 3.086923e-04,\n", - " 3.293162e-04, 3.424217e-04],\n", - " [ 2.896832e-11, 4.326204e-11, 2.680294e-11, ..., 4.254056e-11,\n", - " 7.040103e-11, 9.109586e-11]])\n", + " [ 5.284707e-06, 5.260350e-06, 5.241916e-06, ..., 5.429388e-06,\n", + " 5.373469e-06, 5.316535e-06],\n", + " [ 4.975340e-06, 4.982203e-06, 4.983765e-06, ..., 4.975950e-06,\n", + " 4.970845e-06, 4.972452e-06],\n", + " [ 5.357783e-06, 5.319537e-06, 5.285620e-06, ..., 5.450081e-06,\n", + " 5.420615e-06, 5.394608e-06]])\n", " Coordinates:\n", " * lon (lon) float64 0.0 2.812 5.625 8.438 11.25 14.06 ...\n", " * lat (lat) float64 -87.86 -85.1 -82.31 -79.53 -76.74 ...\n", - " raw_data_start_date datetime64[ns] 1678-01-01\n", - " raw_data_end_date datetime64[ns] 1681-01-01\n", - " subset_start_date datetime64[ns] 1678-01-01\n", - " subset_end_date datetime64[ns] 1680-12-31\n", + " zsurf (lat, lon) float32 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ...\n", + " raw_data_start_date object 0004-01-01 00:00:00\n", + " raw_data_end_date object 0007-01-01 00:00:00\n", + " subset_start_date object 0004-01-01 00:00:00\n", + " subset_end_date object 0006-12-31 00:00:00\n", " sfc_area (lat, lon) float64 3.553e+09 3.553e+09 3.553e+09 ...\n", - " land_mask (lat, lon) float64 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ...,\n", - " 'reg.av': OrderedDict([('globe', \n", - " array(0.5979886366730033)\n", - " Coordinates:\n", - " raw_data_start_date datetime64[ns] 1678-01-01\n", - " raw_data_end_date datetime64[ns] 1681-01-01\n", - " subset_start_date datetime64[ns] 1678-01-01\n", - " subset_end_date datetime64[ns] 1680-12-31),\n", - " ('tropics', \n", - " array(0.8080250991579639)\n", - " Coordinates:\n", - " raw_data_start_date datetime64[ns] 1678-01-01\n", - " raw_data_end_date datetime64[ns] 1681-01-01\n", - " subset_start_date datetime64[ns] 1678-01-01\n", - " subset_end_date datetime64[ns] 1680-12-31)])}" + " land_mask (lat, lon) float64 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ...\n", + " Attributes:\n", + " units: \n", + " description: Sum of convective and large-scale precipitation.\\n\\n Par...,\n", + " 'reg.av': \n", + " Dimensions: ()\n", + " Coordinates:\n", + " raw_data_start_date object 0004-01-01 00:00:00\n", + " raw_data_end_date object 0007-01-01 00:00:00\n", + " subset_start_date object 0004-01-01 00:00:00\n", + " subset_end_date object 0006-12-31 00:00:00\n", + " Data variables:\n", + " tropics float64 4.062e-05\n", + " globe float64 3.501e-05}" ] }, "execution_count": 15, @@ -667,21 +607,11 @@ "execution_count": 16, "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/skc/miniconda/envs/research/lib/python3.6/site-packages/matplotlib/font_manager.py:273: UserWarning: Matplotlib is building the font cache using fc-list. This may take a moment.\n", - " warnings.warn('Matplotlib is building the font cache using fc-list. This may take a moment.')\n", - "/home/skc/miniconda/envs/research/lib/python3.6/site-packages/matplotlib/font_manager.py:273: UserWarning: Matplotlib is building the font cache using fc-list. This may take a moment.\n", - " warnings.warn('Matplotlib is building the font cache using fc-list. This may take a moment.')\n" - ] - }, { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -730,51 +660,59 @@ "name": "stdout", "output_type": "stream", "text": [ - "precip_conv_frac \n", + "precip_total \n", "Dimensions: ()\n", "Coordinates:\n", - " raw_data_start_date datetime64[ns] 1678-01-01\n", - " raw_data_end_date datetime64[ns] 1681-01-01\n", - " subset_start_date datetime64[ns] 1678-01-01\n", - " subset_end_date datetime64[ns] 1680-12-31\n", + " raw_data_start_date object 0004-01-01 00:00:00\n", + " subset_start_date object 0004-01-01 00:00:00\n", + " raw_data_end_date object 0007-01-01 00:00:00\n", + " subset_end_date object 0006-12-31 00:00:00\n", "Data variables:\n", - " globe float64 0.598\n", - " tropics float64 0.808 \n", + " tropics float64 3.51\n", + " globe float64 3.025 \n", "\n", - "precip_total \n", + "precip_conv_frac \n", "Dimensions: ()\n", "Coordinates:\n", - " raw_data_end_date datetime64[ns] 1681-01-01\n", - " subset_start_date datetime64[ns] 1678-01-01\n", - " subset_end_date datetime64[ns] 1680-12-31\n", - " raw_data_start_date datetime64[ns] 1678-01-01\n", + " raw_data_start_date object 0004-01-01 00:00:00\n", + " raw_data_end_date object 0007-01-01 00:00:00\n", + " subset_start_date object 0004-01-01 00:00:00\n", + " subset_end_date object 0006-12-31 00:00:00\n", "Data variables:\n", - " globe float64 3.025\n", - " tropics float64 3.51 \n", + " tropics float64 0.808\n", + " globe float64 0.598 \n", "\n", - "precip_convective \n", + "precip_largescale \n", "Dimensions: ()\n", "Coordinates:\n", - " raw_data_end_date datetime64[ns] 1681-01-01\n", - " subset_start_date datetime64[ns] 1678-01-01\n", - " subset_end_date datetime64[ns] 1680-12-31\n", - " raw_data_start_date datetime64[ns] 1678-01-01\n", + " raw_data_start_date object 0004-01-01 00:00:00\n", + " subset_start_date object 0004-01-01 00:00:00\n", + " raw_data_end_date object 0007-01-01 00:00:00\n", + " subset_end_date object 0006-12-31 00:00:00\n", "Data variables:\n", - " globe float64 2.11\n", - " tropics float64 3.055 \n", + " tropics float64 0.4551\n", + " globe float64 0.9149 \n", "\n", - "precip_largescale \n", + "precip_convective \n", "Dimensions: ()\n", "Coordinates:\n", - " raw_data_end_date datetime64[ns] 1681-01-01\n", - " subset_start_date datetime64[ns] 1678-01-01\n", - " subset_end_date datetime64[ns] 1680-12-31\n", - " raw_data_start_date datetime64[ns] 1678-01-01\n", + " raw_data_start_date object 0004-01-01 00:00:00\n", + " subset_start_date object 0004-01-01 00:00:00\n", + " raw_data_end_date object 0007-01-01 00:00:00\n", + " subset_end_date object 0006-12-31 00:00:00\n", "Data variables:\n", - " globe float64 0.9149\n", - " tropics float64 0.4551 \n", + " tropics float64 3.055\n", + " globe float64 2.11 \n", "\n" ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "//anaconda/envs/aospy_dev/lib/python3.6/site-packages/xarray/core/dataset.py:374: FutureWarning: iteration over an xarray.Dataset will change in xarray v0.11 to only include data variables, not coordinates. Iterate over the Dataset.variables property instead to preserve existing behavior in a forwards compatible manner.\n", + " both_data_and_coords = [k for k in data_vars if k in coords]\n" + ] } ], "source": [ @@ -835,6 +773,13 @@ "import shutil\n", "shutil.rmtree(example_proj.direc_out)" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/aospy/test/test_calc_basic.py b/aospy/test/test_calc_basic.py index a44211b..d66756f 100755 --- a/aospy/test/test_calc_basic.py +++ b/aospy/test/test_calc_basic.py @@ -1,5 +1,6 @@ #!/usr/bin/env python """Basic test of the Calc module on 2D data.""" +import datetime from os.path import isfile import shutil import unittest @@ -46,8 +47,8 @@ def _test_files_and_attrs(calc, dtype_out): 'model': example_model, 'run': example_run, 'var': condensation_rain, - 'date_range': (DatetimeNoLeap(4, 1, 1), - DatetimeNoLeap(6, 12, 31)), + 'date_range': (datetime.datetime(4, 1, 1), + datetime.datetime(6, 12, 31)), 'intvl_in': 'monthly', 'dtype_in_time': 'ts' } diff --git a/aospy/test/test_data_loader.py b/aospy/test/test_data_loader.py index 1ae72d6..df9c5ab 100644 --- a/aospy/test/test_data_loader.py +++ b/aospy/test/test_data_loader.py @@ -1,9 +1,11 @@ #!/usr/bin/env python """Test suite for aospy.data_loader module.""" +import datetime import os import unittest import warnings +import cftime import numpy as np import pytest import xarray as xr @@ -235,8 +237,8 @@ def setUp(self): self.DataLoader = GFDLDataLoader( data_direc=os.path.join('.', 'test'), data_dur=6, - data_start_date=np.datetime64('2000-01-01'), - data_end_date=np.datetime64('2012-12-31'), + data_start_date=datetime.datetime(2000, 1, 1), + data_end_date=datetime.datetime(2012, 12, 31), upcast_float32=False, data_vars='minimal', coords='minimal' @@ -257,12 +259,12 @@ def test_overriding_constructor(self): self.assertEqual(new.data_dur, 8) new = GFDLDataLoader(self.DataLoader, - data_start_date=np.datetime64('2001-01-01')) - self.assertEqual(new.data_start_date, np.datetime64('2001-01-01')) + data_start_date=datetime.datetime(2001, 1, 1)) + self.assertEqual(new.data_start_date, datetime.datetime(2001, 1, 1)) new = GFDLDataLoader(self.DataLoader, - data_end_date=np.datetime64('2003-12-31')) - self.assertEqual(new.data_end_date, np.datetime64('2003-12-31')) + data_end_date=datetime.datetime(2003, 12, 31)) + self.assertEqual(new.data_end_date, datetime.datetime(2003, 12, 31)) new = GFDLDataLoader(self.DataLoader, preprocess_func=lambda ds: ds) @@ -323,16 +325,16 @@ def test_input_data_paths_gfdl(self): '6yr', 'atmos_daily.20060101-20111231.temp.nc')] result = self.DataLoader._input_data_paths_gfdl( - 'temp', np.datetime64('2010-01-01'), - np.datetime64('2010-12-31'), 'atmos', + 'temp', datetime.datetime(2010, 1, 1), + datetime.datetime(2010, 12, 31), 'atmos', 'daily', 'pressure', 'ts', None) self.assertEqual(result, expected) expected = [os.path.join('.', 'test', 'atmos_level', 'ts', 'monthly', '6yr', 'atmos_level.200601-201112.temp.nc')] result = self.DataLoader._input_data_paths_gfdl( - 'temp', np.datetime64('2010-01-01'), - np.datetime64('2010-12-31'), 'atmos', + 'temp', cftime.DatetimeNoLeap(2010, 1, 1), + cftime.DatetimeNoLeap(2010, 12, 31), 'atmos', 'monthly', ETA_STR, 'ts', None) self.assertEqual(result, expected) @@ -486,6 +488,26 @@ def test_load_variable(self): expected = _open_ds_catch_warnings(filepath)['condensation_rain'] np.testing.assert_array_equal(result.values, expected.values) + def test_load_variable_datetime_datetime(self): + result = self.data_loader.load_variable( + condensation_rain, datetime.datetime(5, 1, 1), + datetime.datetime(5, 12, 31), + intvl_in='monthly') + filepath = os.path.join(os.path.split(ROOT_PATH)[0], 'netcdf', + '00050101.precip_monthly.nc') + expected = _open_ds_catch_warnings(filepath)['condensation_rain'] + np.testing.assert_array_equal(result.values, expected.values) + + def test_load_variable_datetime64(self): + result = self.data_loader.load_variable( + condensation_rain, np.datetime64('0005-01-01'), + np.datetime64('0005-12-31'), + intvl_in='monthly') + filepath = os.path.join(os.path.split(ROOT_PATH)[0], 'netcdf', + '00050101.precip_monthly.nc') + expected = _open_ds_catch_warnings(filepath)['condensation_rain'] + np.testing.assert_array_equal(result.values, expected.values) + def test_load_variable_does_not_warn(self): with warnings.catch_warnings(record=True) as warnlog: self.data_loader.load_variable(condensation_rain, diff --git a/aospy/test/test_utils_times.py b/aospy/test/test_utils_times.py index 7c507f7..ee0ab65 100755 --- a/aospy/test/test_utils_times.py +++ b/aospy/test/test_utils_times.py @@ -31,7 +31,8 @@ ensure_time_as_index, sel_time, yearly_average, - maybe_cast_as_timestamp + maybe_cast_as_timestamp, + maybe_convert_to_index_date_type ) @@ -110,7 +111,8 @@ def test_monthly_mean_at_each_ind(): @pytest.mark.parametrize('date', [np.datetime64('2000-01-01'), - cftime.DatetimeNoLeap(1, 1, 1)]) + cftime.DatetimeNoLeap(1, 1, 1), + datetime.datetime(1, 1, 1)]) def test_ensure_datetime_valid_input(date): assert ensure_datetime(date) == date @@ -521,3 +523,34 @@ def test_maybe_cast_as_timestamp_cftime_datetime(): expected = date result = maybe_cast_as_timestamp(date) assert result == expected + + +DATETIME_INDEX = pd.date_range('2000-01-01', freq='M', periods=1) +CFTIME_INDEX = xr.CFTimeIndex([cftime.DatetimeNoLeap(1, 1, 1)]) +CONVERT_DATE_TYPE_TESTS = { + 'DatetimeIndex-np.datetime64': + (DATETIME_INDEX, np.datetime64('2000-01'), np.datetime64('2000-01')), + 'DatetimeIndex-datetime.datetime': + (DATETIME_INDEX, datetime.datetime(2000, 1, 1), np.datetime64('2000-01')), + 'DatetimeIndex-cftime.DatetimeGregorian': + (DATETIME_INDEX, cftime.DatetimeGregorian(2000, 1, 1), + np.datetime64('2000-01')), + 'CFTimeIndex[DatetimeNoLeap]-np.datetime64': + (CFTIME_INDEX, np.datetime64('0001-01'), cftime.DatetimeNoLeap(1, 1, 1)), + 'CFTimeIndex[DatetimeNoLeap]-datetime.datetime': + (CFTIME_INDEX, datetime.datetime(1, 1, 1), cftime.DatetimeNoLeap(1, 1, 1)), + 'CFTimeIndex[DatetimeNoLeap]-cftime.DatetimeNoLeap': + (CFTIME_INDEX, cftime.DatetimeNoLeap(1, 1, 1), + cftime.DatetimeNoLeap(1, 1, 1)), + 'CFTimeIndex[DatetimeNoLeap]-cftime.DatetimeGregorian': + (CFTIME_INDEX, cftime.DatetimeGregorian(1, 1, 1), + cftime.DatetimeNoLeap(1, 1, 1)) +} + + +@pytest.mark.parametrize(['index', 'date', 'expected'], + list(CONVERT_DATE_TYPE_TESTS.values()), + ids=list(CONVERT_DATE_TYPE_TESTS.keys())) +def test_maybe_convert_to_index_date_type(index, date, expected): + result = maybe_convert_to_index_date_type(index, date) + assert result == expected diff --git a/aospy/utils/times.py b/aospy/utils/times.py index a187de4..a30c597 100644 --- a/aospy/utils/times.py +++ b/aospy/utils/times.py @@ -188,14 +188,14 @@ def ensure_datetime(obj): ------ TypeError if `obj` is not datetime-like """ - if isinstance(obj, (cftime.datetime, np.datetime64)): + if isinstance(obj, (datetime.datetime, cftime.datetime, np.datetime64)): return obj raise TypeError("datetime-like object required. " "Type given: {}".format(type(obj))) def datetime_or_default(date, default): - """Return a datetime.datetime object or a default. + """Return a datetime-like object or a default. Parameters ---------- @@ -516,3 +516,39 @@ def maybe_cast_as_timestamp(date): except TypeError: return date + +def maybe_convert_to_index_date_type(index, date): + """Convert a datetime-like object to the appropriate type + + Parameters + ---------- + index : pd.Index + Input time index + date : datetime-like object + Input datetime + + Returns + ------- + date of the type appropriate for the time index of the Dataset + """ + if isinstance(index, pd.DatetimeIndex): + if isinstance(date, np.datetime64): + return date + else: + return np.datetime64(str(date)) + else: + date_type = index.date_type + if isinstance(date, date_type): + return date + else: + if isinstance(date, np.datetime64): + # Convert to datetime.date or datetime.datetime object + date = date.item() + + if isinstance(date, datetime.date): + # Convert to a datetime.datetime object + date = datetime.datetime.combine( + date, datetime.datetime.min.time()) + + return date_type(date.year, date.month, date.day, date.hour, + date.minute, date.second, date.microsecond) From 72494b13f8b9caf8071b65167dfeeea877e0430e Mon Sep 17 00:00:00 2001 From: Spencer Clark Date: Sun, 20 May 2018 09:45:22 -0400 Subject: [PATCH 10/17] Bump required xarray version to 0.10.4 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index bb659f5..0da5e21 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ 'toolz >= 0.7.2', 'dask >= 0.14', 'distributed >= 1.17.1', - 'xarray >= 0.10.3', + 'xarray >= 0.10.4', 'cloudpickle >= 0.2.1'], tests_require=['pytest >= 2.7.1', 'pytest-catchlog >= 1.0'], From 916605627c13fa5de2d7559905e8cf1445997913 Mon Sep 17 00:00:00 2001 From: Spencer Clark Date: Sun, 20 May 2018 09:53:37 -0400 Subject: [PATCH 11/17] Clean up test_data_loader.py --- aospy/test/test_data_loader.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/aospy/test/test_data_loader.py b/aospy/test/test_data_loader.py index df9c5ab..b4ac965 100644 --- a/aospy/test/test_data_loader.py +++ b/aospy/test/test_data_loader.py @@ -5,7 +5,6 @@ import unittest import warnings -import cftime import numpy as np import pytest import xarray as xr @@ -333,8 +332,8 @@ def test_input_data_paths_gfdl(self): expected = [os.path.join('.', 'test', 'atmos_level', 'ts', 'monthly', '6yr', 'atmos_level.200601-201112.temp.nc')] result = self.DataLoader._input_data_paths_gfdl( - 'temp', cftime.DatetimeNoLeap(2010, 1, 1), - cftime.DatetimeNoLeap(2010, 12, 31), 'atmos', + 'temp', DatetimeNoLeap(2010, 1, 1), + DatetimeNoLeap(2010, 12, 31), 'atmos', 'monthly', ETA_STR, 'ts', None) self.assertEqual(result, expected) From 36e020b7d5f460fea6643467875d2d2f3286bdb6 Mon Sep 17 00:00:00 2001 From: Spencer Clark Date: Sun, 20 May 2018 11:38:58 -0400 Subject: [PATCH 12/17] Add support for string date ranges --- aospy/calc.py | 4 +- aospy/data_loader.py | 10 ++--- aospy/test/test_calc_basic.py | 3 +- aospy/test/test_data_loader.py | 16 ++++++++ aospy/test/test_run.py | 2 +- aospy/test/test_utils_times.py | 73 ++++++++++++++++++++++++---------- aospy/utils/times.py | 47 +++++++++++++++++++--- 7 files changed, 120 insertions(+), 35 deletions(-) diff --git a/aospy/calc.py b/aospy/calc.py index e8f4e6c..aa23eb6 100644 --- a/aospy/calc.py +++ b/aospy/calc.py @@ -82,7 +82,9 @@ def _file_name(self, dtype_out_time, extension='nc'): dtype_vert=self.dtype_out_vert) in_lbl = utils.io.data_in_label(self.intvl_in, self.dtype_in_time, self.dtype_in_vert) - yr_lbl = utils.io.yr_label((self.start_date.year, self.end_date.year)) + start_year = utils.times.infer_year(self.start_date) + end_year = utils.times.infer_year(self.end_date) + yr_lbl = utils.io.yr_label((start_year, end_year)) return '.'.join( [self.name, out_lbl, in_lbl, self.model.name, self.run.name, yr_lbl, extension] diff --git a/aospy/data_loader.py b/aospy/data_loader.py index 51e5e99..077bb69 100644 --- a/aospy/data_loader.py +++ b/aospy/data_loader.py @@ -605,13 +605,13 @@ def _input_data_paths_gfdl(self, name, start_date, end_date, domain, else: subdir = os.path.join(intvl_in, dur_str) direc = os.path.join(self.data_direc, domain, dtype_lbl, subdir) - data_start_date = times.maybe_cast_as_timestamp(self.data_start_date) - start_date = times.maybe_cast_as_timestamp(start_date) - end_date = times.maybe_cast_as_timestamp(end_date) + data_start_year = times.infer_year(self.data_start_date) + start_year = times.infer_year(start_date) + end_year = times.infer_year(end_date) files = [os.path.join(direc, io.data_name_gfdl( name, domain, dtype, intvl_in, year, intvl_out, - data_start_date.year, self.data_dur)) - for year in range(start_date.year, end_date.year + 1)] + data_start_year, self.data_dur)) + for year in range(start_year, end_year + 1)] files = list(set(files)) files.sort() return files diff --git a/aospy/test/test_calc_basic.py b/aospy/test/test_calc_basic.py index d66756f..9f81664 100755 --- a/aospy/test/test_calc_basic.py +++ b/aospy/test/test_calc_basic.py @@ -121,8 +121,7 @@ def setUp(self): 'model': example_model, 'run': example_run, 'var': precip, - 'date_range': (DatetimeNoLeap(4, 1, 1), - DatetimeNoLeap(6, 12, 31)), + 'date_range': ('0004', '0006'), 'intvl_in': 'monthly', 'dtype_in_time': 'ts' } diff --git a/aospy/test/test_data_loader.py b/aospy/test/test_data_loader.py index b4ac965..7728371 100644 --- a/aospy/test/test_data_loader.py +++ b/aospy/test/test_data_loader.py @@ -329,6 +329,14 @@ def test_input_data_paths_gfdl(self): 'daily', 'pressure', 'ts', None) self.assertEqual(result, expected) + expected = [os.path.join('.', 'test', 'atmos_daily', 'ts', 'daily', + '6yr', + 'atmos_daily.20060101-20111231.temp.nc')] + result = self.DataLoader._input_data_paths_gfdl( + 'temp', '2010', '2010', 'atmos', + 'daily', 'pressure', 'ts', None) + self.assertEqual(result, expected) + expected = [os.path.join('.', 'test', 'atmos_level', 'ts', 'monthly', '6yr', 'atmos_level.200601-201112.temp.nc')] result = self.DataLoader._input_data_paths_gfdl( @@ -507,6 +515,14 @@ def test_load_variable_datetime64(self): expected = _open_ds_catch_warnings(filepath)['condensation_rain'] np.testing.assert_array_equal(result.values, expected.values) + def test_load_variable_str(self): + result = self.data_loader.load_variable( + condensation_rain, '0005', '0005', intvl_in='monthly') + filepath = os.path.join(os.path.split(ROOT_PATH)[0], 'netcdf', + '00050101.precip_monthly.nc') + expected = _open_ds_catch_warnings(filepath)['condensation_rain'] + np.testing.assert_array_equal(result.values, expected.values) + def test_load_variable_does_not_warn(self): with warnings.catch_warnings(record=True) as warnlog: self.data_loader.load_variable(condensation_rain, diff --git a/aospy/test/test_run.py b/aospy/test/test_run.py index 2ef8c37..aec1d17 100644 --- a/aospy/test/test_run.py +++ b/aospy/test/test_run.py @@ -27,7 +27,7 @@ def test_init_dates_valid_input(self): def test_init_dates_invalid_input(self): for attr in ['default_start_date', 'default_end_date']: - for date in [1985, False, '1750-12-10']: + for date in [1985, False]: with self.assertRaises(TypeError): Run(**{attr: date}) diff --git a/aospy/test/test_utils_times.py b/aospy/test/test_utils_times.py index ee0ab65..4483220 100755 --- a/aospy/test/test_utils_times.py +++ b/aospy/test/test_utils_times.py @@ -31,12 +31,12 @@ ensure_time_as_index, sel_time, yearly_average, - maybe_cast_as_timestamp, + infer_year, maybe_convert_to_index_date_type ) -_INVALID_DATE_OBJECTS = [1985, True, None, '2016-04-07'] +_INVALID_DATE_OBJECTS = [1985, True, None] def test_apply_time_offset(): @@ -112,7 +112,8 @@ def test_monthly_mean_at_each_ind(): @pytest.mark.parametrize('date', [np.datetime64('2000-01-01'), cftime.DatetimeNoLeap(1, 1, 1), - datetime.datetime(1, 1, 1)]) + datetime.datetime(1, 1, 1), + '2000-01-01']) def test_ensure_datetime_valid_input(date): assert ensure_datetime(date) == date @@ -368,6 +369,40 @@ def test_assert_has_data_for_time_cftime_datetimes(calendar, date_type): _assert_has_data_for_time(da, start_date_bad, end_date_bad) +def test_assert_has_data_for_time_str_input(): + time_bounds = np.array([[0, 31], [31, 59], [59, 90]]) + nv = np.array([0, 1]) + time = np.array([15, 46, 74]) + data = np.zeros((3)) + var_name = 'a' + ds = xr.DataArray(data, + coords=[time], + dims=[TIME_STR], + name=var_name).to_dataset() + ds[TIME_BOUNDS_STR] = xr.DataArray(time_bounds, + coords=[time, nv], + dims=[TIME_STR, BOUNDS_STR], + name=TIME_BOUNDS_STR) + units_str = 'days since 2000-01-01 00:00:00' + ds[TIME_STR].attrs['units'] = units_str + ds = ensure_time_avg_has_cf_metadata(ds) + ds = set_grid_attrs_as_coords(ds) + ds = xr.decode_cf(ds) + da = ds[var_name] + + start_date = '2000-01-01' + end_date = '2000-03-31' + _assert_has_data_for_time(da, start_date, end_date) + + start_date_bad = '1999-12-31' + end_date_bad = '2000-04-01' + + # With strings these checks are disabled + _assert_has_data_for_time(da, start_date_bad, end_date) + _assert_has_data_for_time(da, start_date, end_date_bad) + _assert_has_data_for_time(da, start_date_bad, end_date_bad) + + def test_assert_matching_time_coord(): rng = pd.date_range('2000-01-01', '2001-01-01', freq='M') arr1 = xr.DataArray(rng, coords=[rng], dims=[TIME_STR]) @@ -504,25 +539,19 @@ def test_average_time_bounds(ds_time_encoded_cf): xr.testing.assert_identical(actual, desired) -def test_maybe_cast_as_timestamp_datetime64(): - date = np.datetime64('2000-01-01') - expected = pd.Timestamp(date) - result = maybe_cast_as_timestamp(date) - assert result == expected - - -def test_maybe_cast_as_timestamp_string(): - date = '2000-01-01' - expected = pd.Timestamp(date) - result = maybe_cast_as_timestamp(date) - assert result == expected +@pytest.mark.parametrize( + ['date', 'expected'], + [(np.datetime64('2000-01-01'), 2000), + ('2000-01-01', 2000), + (cftime.DatetimeNoLeap(2000, 1, 1), 2000)]) +def test_infer_year(date, expected): + assert infer_year(date) == expected -def test_maybe_cast_as_timestamp_cftime_datetime(): - date = cftime.DatetimeNoLeap(2000, 1, 1) - expected = date - result = maybe_cast_as_timestamp(date) - assert result == expected +@pytest.mark.parametrize('date', ['-0001', 'A001']) +def test_infer_year_invalid(date): + with pytest.raises(ValueError): + infer_year(date) DATETIME_INDEX = pd.date_range('2000-01-01', freq='M', periods=1) @@ -535,6 +564,7 @@ def test_maybe_cast_as_timestamp_cftime_datetime(): 'DatetimeIndex-cftime.DatetimeGregorian': (DATETIME_INDEX, cftime.DatetimeGregorian(2000, 1, 1), np.datetime64('2000-01')), + 'DatetimeIndex-str': (DATETIME_INDEX, '2000', '2000'), 'CFTimeIndex[DatetimeNoLeap]-np.datetime64': (CFTIME_INDEX, np.datetime64('0001-01'), cftime.DatetimeNoLeap(1, 1, 1)), 'CFTimeIndex[DatetimeNoLeap]-datetime.datetime': @@ -544,7 +574,8 @@ def test_maybe_cast_as_timestamp_cftime_datetime(): cftime.DatetimeNoLeap(1, 1, 1)), 'CFTimeIndex[DatetimeNoLeap]-cftime.DatetimeGregorian': (CFTIME_INDEX, cftime.DatetimeGregorian(1, 1, 1), - cftime.DatetimeNoLeap(1, 1, 1)) + cftime.DatetimeNoLeap(1, 1, 1)), + 'CFTimeIndex-str': (CFTIME_INDEX, '0001', '0001') } diff --git a/aospy/utils/times.py b/aospy/utils/times.py index a30c597..6a5e9a8 100644 --- a/aospy/utils/times.py +++ b/aospy/utils/times.py @@ -1,5 +1,6 @@ """Utility functions for handling times, dates, etc.""" import datetime +import re import warnings import cftime @@ -7,6 +8,8 @@ import pandas as pd import xarray as xr +from pandas.errors import OutOfBoundsDatetime + from ..internal_names import ( BOUNDS_STR, GRID_ATTRS_NO_TIMES, RAW_END_DATE_STR, RAW_START_DATE_STR, SUBSET_END_DATE_STR, SUBSET_START_DATE_STR, TIME_BOUNDS_STR, TIME_STR, @@ -188,7 +191,7 @@ def ensure_datetime(obj): ------ TypeError if `obj` is not datetime-like """ - if isinstance(obj, (datetime.datetime, cftime.datetime, np.datetime64)): + if isinstance(obj, (str, datetime.datetime, cftime.datetime, np.datetime64)): return obj raise TypeError("datetime-like object required. " "Type given: {}".format(type(obj))) @@ -394,6 +397,9 @@ def _assert_has_data_for_time(da, start_date, end_date): AssertionError if the time range is not within the time range of the DataArray """ + if isinstance(start_date, str) and isinstance(end_date, str): + return + if RAW_START_DATE_STR in da.coords: da_start = da[RAW_START_DATE_STR].values da_end = da[RAW_END_DATE_STR].values @@ -499,6 +505,31 @@ def ensure_time_as_index(ds): return ds +def infer_year(date): + """Given a datetime-like object or string infer the year + + Parameters + ---------- + date : str + Input date + + Returns + ------- + int + """ + if isinstance(date, str): + pattern = '(?P\d{4})' + result = re.match(pattern, date) + if result: + return int(result.groupdict()['year']) + else: + raise ValueError('Invalid date string provided: {}'.format(date)) + elif isinstance(date, np.datetime64): + return date.item().year + else: + return date.year + + def maybe_cast_as_timestamp(date): """Convert a date to a pd.Timestamp object if possible @@ -511,10 +542,13 @@ def maybe_cast_as_timestamp(date): ------- pd.Timestamp or cftime.datetime """ - try: - return pd.to_datetime(date) - except TypeError: - return date + if isinstance(date, str): + return np.datetime64(date).item() + else: + try: + return pd.to_datetime(date) + except (TypeError, OutOfBoundsDatetime): + return date def maybe_convert_to_index_date_type(index, date): @@ -531,6 +565,9 @@ def maybe_convert_to_index_date_type(index, date): ------- date of the type appropriate for the time index of the Dataset """ + if isinstance(date, str): + return date + if isinstance(index, pd.DatetimeIndex): if isinstance(date, np.datetime64): return date From c9d3aad6858b0421a3087577418f674234b9e2b5 Mon Sep 17 00:00:00 2001 From: Spencer Clark Date: Mon, 28 May 2018 11:16:30 -0400 Subject: [PATCH 13/17] Document enhancements to datetime handling --- docs/examples.rst | 60 ++++++++++++++++++++++++++-------------------- docs/whats-new.rst | 11 ++++++--- 2 files changed, 42 insertions(+), 29 deletions(-) diff --git a/docs/examples.rst b/docs/examples.rst index 6402eba..a5e4808 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -97,9 +97,28 @@ a name for the run and an optional description. example_run = Run( name='example_run', description='Control simulation of the idealized moist model', - data_loader=data_loader + data_loader=data_loader, + default_start_date='0004', + default_end_date='0006' ) +.. note:: + + Throughout aospy, date slice bounds can be specified with dates of any of + the following types: + + - ``str``, for partial-datetime string indexing + - ``np.datetime64`` + - ``datetime.datetime`` + - ``cftime.datetime`` + + If possible, aospy will automatically convert the latter three to the + appropriate date type used for indexing the data read in; otherwise it will + raise an error. Therefore the arguments ``default_start_date`` and + ``default_end_date`` in the ``Run`` constructor are calendar-agnostic (as + are the ``date_ranges`` specified :ref:`when submitting + calculations`). + .. note:: See the :ref:`API reference ` for other optional arguments @@ -189,18 +208,18 @@ that we saw are directly available as model output: from aospy import Var - precip_largescale = Var( - name='precip_largescale', # name used by aospy - alt_names=('condensation_rain',), # its possible name(s) in your data - def_time=True, # whether or not it is defined in time - description='Precipitation generated via grid-scale condensation', - ) - precip_convective = Var( - name='precip_convective', - alt_names=('convection_rain', 'prec_conv'), - def_time=True, - description='Precipitation generated by convective parameterization', - ) + precip_largescale = Var( + name='precip_largescale', # name used by aospy + alt_names=('condensation_rain',), # its possible name(s) in your data + def_time=True, # whether or not it is defined in time + description='Precipitation generated via grid-scale condensation', + ) + precip_convective = Var( + name='precip_convective', + alt_names=('convection_rain', 'prec_conv'), + def_time=True, + description='Precipitation generated by convective parameterization', + ) When it comes time to load data corresponding to either of these from one or more particular netCDF files, aospy will search for variables @@ -338,6 +357,8 @@ Submitting calculations Using :py:func:`aospy.submit_mult_calcs` ======================================== +.. _Submitting calculations: + Having put in the legwork above of describing our data and the physical quantities we wish to compute, we can submit our desired calculations for execution using :py:func:`aospy.submit_mult_calcs`. @@ -432,19 +453,6 @@ and the results of each output type quantity is, even if you don't have the original ``Var`` definition on hand. -.. note:: - - You may have noticed that ``subset_...`` and ``raw_...`` - coordinates have years 1678 and later, when our data was from model - years 4 through 6. This is because `technical details upstream - `_ limit the range of supported whole years to 1678-2262. - - As a workaround, aospy pretends that any timeseries that starts - before the beginning of this range actually starts at 1678. An - upstream fix is `currently under way - `_, at which point - all dates will be supported without this workaround. - Gridpoint-by-gridpoint ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/whats-new.rst b/docs/whats-new.rst index 66af034..3a42699 100644 --- a/docs/whats-new.rst +++ b/docs/whats-new.rst @@ -44,6 +44,11 @@ Documentation Enhancements ~~~~~~~~~~~~ +- Use an ``xarray.CFTimeIndex`` for dates from non-standard calendars and + outside the Timestamp-valid range. This eliminates the need for the prior + workaround, which shifted dates to within the range 1678 to 2262 prior to + indexing (closes :issue:`98` via :pull:`273`). By + `Spencer Clark `_. - Create ``utils.longitude`` module and ``Longitude`` class for representing and comparing longitudes. Used internally by ``aospy.Region`` to construct masks, but could also be useful for @@ -137,9 +142,9 @@ Dependencies - ``aospy`` now requires a minimum version of ``distributed`` of 1.17.1 (fixes :issue:`210` via :pull:`211`). -- ``aospy`` now requires a minimum version of ``xarray`` of 0.10.3. - See discussion in :issue:`199`, :pull:`240`, :issue:`268`, and - :pull:`269` for more details. +- ``aospy`` now requires a minimum version of ``xarray`` of 0.10.4. + See discussion in :issue:`199`, :pull:`240`, :issue:`268`, + :pull:`269`, and :pull:`273` for more details. .. _whats-new.0.2: From f4dd4da26e1f94474aeb87e953a989475b610e74 Mon Sep 17 00:00:00 2001 From: Spencer Clark Date: Mon, 28 May 2018 15:56:26 -0400 Subject: [PATCH 14/17] Improve tests --- aospy/test/test_calc_basic.py | 239 +++--- aospy/test/test_data_loader.py | 1370 +++++++++++++++++--------------- 2 files changed, 860 insertions(+), 749 deletions(-) diff --git a/aospy/test/test_calc_basic.py b/aospy/test/test_calc_basic.py index 9f81664..b36a530 100755 --- a/aospy/test/test_calc_basic.py +++ b/aospy/test/test_calc_basic.py @@ -7,10 +7,10 @@ import pytest import itertools +import cftime +import numpy as np import xarray as xr -from cftime import DatetimeNoLeap - from aospy import Var from aospy.calc import Calc, _add_metadata_as_attrs from .data.objects.examples import ( @@ -42,108 +42,122 @@ def _test_files_and_attrs(calc, dtype_out): _test_output_attrs(calc, dtype_out) -_BASIC_TEST_PARAMS = { - 'proj': example_proj, - 'model': example_model, - 'run': example_run, - 'var': condensation_rain, - 'date_range': (datetime.datetime(4, 1, 1), - datetime.datetime(6, 12, 31)), - 'intvl_in': 'monthly', - 'dtype_in_time': 'ts' +_2D_DATE_RANGES = { + 'datetime': (datetime.datetime(4, 1, 1), datetime.datetime(6, 12, 31)), + 'datetime64': (np.datetime64('0004-01-01'), np.datetime64('0006-12-31')), + 'cftime': (cftime.DatetimeNoLeap(4, 1, 1), + cftime.DatetimeNoLeap(6, 12, 31)), + 'str': ('0004', '0006') +} +_3D_DATE_RANGES = { + 'datetime': (datetime.datetime(6, 1, 1), datetime.datetime(6, 1, 31)), + 'datetime64': (np.datetime64('0006-01-01'), np.datetime64('0006-01-31')), + 'cftime': (cftime.DatetimeNoLeap(6, 1, 1), + cftime.DatetimeNoLeap(6, 1, 31)) + # 'str': ('0006', '0006') TODO: This should work once xarray version + # 0.10.5 is released } +_2D_VARS = {'basic': condensation_rain, 'composite': precip} +_2D_DTYPE_OUT_VERT = {'None': None} +_2D_DTYPE_IN_VERT = {'None': None} +_3D_VARS = {'3D': sphum} +_3D_DTYPE_OUT_VERT = {'vert_int': 'vert_int'} +_3D_DTYPE_IN_VERT = {'sigma': 'sigma'} +_CASES = ( + list(itertools.product(_2D_DATE_RANGES.items(), _2D_VARS.items(), + _2D_DTYPE_IN_VERT.items(), + _2D_DTYPE_OUT_VERT.items())) + + list(itertools.product(_3D_DATE_RANGES.items(), _3D_VARS.items(), + _3D_DTYPE_IN_VERT.items(), + _3D_DTYPE_OUT_VERT.items())) +) +_CALC_TESTS = {} +for ((date_type, date_range), (test_type, var), + (vert_in_label, vert_in), (vert_out_label, vert_out)) in _CASES: + _CALC_TESTS['{}-{}-{}-{}'.format( + date_type, test_type, vert_in_label, vert_out_label)] = ( + date_range, var, vert_in, vert_out) + + +@pytest.fixture(params=_CALC_TESTS.values(), ids=list(_CALC_TESTS.keys())) +def test_params(request): + date_range, var, vert_in, vert_out = request.param + yield { + 'proj': example_proj, + 'model': example_model, + 'run': example_run, + 'var': var, + 'date_range': date_range, + 'intvl_in': 'monthly', + 'dtype_in_time': 'ts', + 'dtype_in_vert': vert_in, + 'dtype_out_vert': vert_out + } + for direc in [example_proj.direc_out, example_proj.tar_direc_out]: + try: + shutil.rmtree(direc) + except OSError: + pass + + +def test_annual_mean(test_params): + calc = Calc(intvl_out='ann', dtype_out_time='av', **test_params) + calc.compute() + _test_files_and_attrs(calc, 'av') + + +def test_annual_ts(test_params): + calc = Calc(intvl_out='ann', dtype_out_time='ts', **test_params) + calc.compute() + _test_files_and_attrs(calc, 'ts') + + +def test_seasonal_mean(test_params): + calc = Calc(intvl_out='djf', dtype_out_time='av', **test_params) + calc.compute() + _test_files_and_attrs(calc, 'av') + + +def test_seasonal_ts(test_params): + calc = Calc(intvl_out='djf', dtype_out_time='ts', **test_params) + calc.compute() + _test_files_and_attrs(calc, 'ts') + + +def test_monthly_mean(test_params): + calc = Calc(intvl_out=1, dtype_out_time='av', **test_params) + calc.compute() + _test_files_and_attrs(calc, 'av') + + +def test_monthly_ts(test_params): + calc = Calc(intvl_out=1, dtype_out_time='ts', **test_params) + calc.compute() + _test_files_and_attrs(calc, 'ts') + + +def test_simple_reg_av(test_params): + calc = Calc(intvl_out='ann', dtype_out_time='reg.av', region=[globe], + **test_params) + calc.compute() + _test_files_and_attrs(calc, 'reg.av') + + +def test_simple_reg_ts(test_params): + calc = Calc(intvl_out='ann', dtype_out_time='reg.ts', region=[globe], + **test_params) + calc.compute() + _test_files_and_attrs(calc, 'reg.ts') + + +def test_complex_reg_av(test_params): + calc = Calc(intvl_out='ann', dtype_out_time='reg.av', region=[sahel], + **test_params) + calc.compute() + _test_files_and_attrs(calc, 'reg.av') -class TestCalcBasic(unittest.TestCase): - def setUp(self): - self.test_params = _BASIC_TEST_PARAMS.copy() - - def tearDown(self): - for direc in [example_proj.direc_out, example_proj.tar_direc_out]: - try: - shutil.rmtree(direc) - except OSError: - pass - - def test_annual_mean(self): - calc = Calc(intvl_out='ann', dtype_out_time='av', **self.test_params) - calc.compute() - _test_files_and_attrs(calc, 'av') - - def test_annual_ts(self): - calc = Calc(intvl_out='ann', dtype_out_time='ts', **self.test_params) - calc.compute() - _test_files_and_attrs(calc, 'ts') - - def test_seasonal_mean(self): - calc = Calc(intvl_out='djf', dtype_out_time='av', **self.test_params) - calc.compute() - _test_files_and_attrs(calc, 'av') - - def test_seasonal_ts(self): - calc = Calc(intvl_out='djf', dtype_out_time='ts', **self.test_params) - calc.compute() - _test_files_and_attrs(calc, 'ts') - - def test_monthly_mean(self): - calc = Calc(intvl_out=1, dtype_out_time='av', **self.test_params) - calc.compute() - _test_files_and_attrs(calc, 'av') - - def test_monthly_ts(self): - calc = Calc(intvl_out=1, dtype_out_time='ts', **self.test_params) - calc.compute() - _test_files_and_attrs(calc, 'ts') - - def test_simple_reg_av(self): - calc = Calc(intvl_out='ann', dtype_out_time='reg.av', region=[globe], - **self.test_params) - calc.compute() - _test_files_and_attrs(calc, 'reg.av') - - def test_simple_reg_ts(self): - calc = Calc(intvl_out='ann', dtype_out_time='reg.ts', region=[globe], - **self.test_params) - calc.compute() - _test_files_and_attrs(calc, 'reg.ts') - - def test_complex_reg_av(self): - calc = Calc(intvl_out='ann', dtype_out_time='reg.av', region=[sahel], - **self.test_params) - calc.compute() - _test_files_and_attrs(calc, 'reg.av') - - -class TestCalcComposite(TestCalcBasic): - def setUp(self): - self.test_params = { - 'proj': example_proj, - 'model': example_model, - 'run': example_run, - 'var': precip, - 'date_range': ('0004', '0006'), - 'intvl_in': 'monthly', - 'dtype_in_time': 'ts' - } - - -class TestCalc3D(TestCalcBasic): - def setUp(self): - self.test_params = { - 'proj': example_proj, - 'model': example_model, - 'run': example_run, - 'var': sphum, - 'date_range': (DatetimeNoLeap(6, 1, 1), - DatetimeNoLeap(6, 1, 31)), - 'intvl_in': 'monthly', - 'dtype_in_time': 'ts', - 'dtype_in_vert': 'sigma', - 'dtype_out_vert': 'vert_int' - } - - -test_params = { +test_params_not_time_defined = { 'proj': example_proj, 'model': example_model, 'run': example_run, @@ -157,8 +171,8 @@ def setUp(self): @pytest.mark.parametrize('dtype_out_time', [None, []]) def test_calc_object_no_time_options(dtype_out_time): - test_params['dtype_out_time'] = dtype_out_time - calc = Calc(**test_params) + test_params_not_time_defined['dtype_out_time'] = dtype_out_time + calc = Calc(**test_params_not_time_defined) if isinstance(dtype_out_time, list): assert calc.dtype_out_time == tuple(dtype_out_time) else: @@ -169,9 +183,9 @@ def test_calc_object_no_time_options(dtype_out_time): 'dtype_out_time', ['av', 'std', 'ts', 'reg.av', 'reg.std', 'reg.ts']) def test_calc_object_string_time_options(dtype_out_time): - test_params['dtype_out_time'] = dtype_out_time + test_params_not_time_defined['dtype_out_time'] = dtype_out_time with pytest.raises(ValueError): - Calc(**test_params) + Calc(**test_params_not_time_defined) def test_calc_object_time_options(): @@ -179,9 +193,9 @@ def test_calc_object_time_options(): for i in range(1, len(time_options) + 1): for time_option in list(itertools.permutations(time_options, i)): if time_option != ('None',): - test_params['dtype_out_time'] = time_option + test_params_not_time_defined['dtype_out_time'] = time_option with pytest.raises(ValueError): - Calc(**test_params) + Calc(**test_params_not_time_defined) @pytest.mark.parametrize( @@ -218,7 +232,16 @@ def test_attrs(units, description, dtype_out_vert, expected_units, @pytest.fixture() def recursive_test_params(): - basic_params = _BASIC_TEST_PARAMS.copy() + basic_params = { + 'proj': example_proj, + 'model': example_model, + 'run': example_run, + 'var': condensation_rain, + 'date_range': (datetime.datetime(4, 1, 1), + datetime.datetime(6, 12, 31)), + 'intvl_in': 'monthly', + 'dtype_in_time': 'ts' + } recursive_params = basic_params.copy() recursive_condensation_rain = Var( diff --git a/aospy/test/test_data_loader.py b/aospy/test/test_data_loader.py index 7728371..38133e5 100644 --- a/aospy/test/test_data_loader.py +++ b/aospy/test/test_data_loader.py @@ -23,7 +23,7 @@ TIME_WEIGHTS_STR, GRID_ATTRS, ZSURF_STR) from aospy.utils import io from .data.objects.examples import (condensation_rain, convection_rain, precip, - example_run, ROOT_PATH) + file_map, ROOT_PATH) def _open_ds_catch_warnings(path): @@ -41,663 +41,751 @@ def test_maybe_cast_to_float64(input_dtype, expected_dtype): assert result == expected_dtype -class AospyDataLoaderTestCase(unittest.TestCase): - def setUp(self): - self.DataLoader = DataLoader() - self.generate_file_set_args = dict( - var=condensation_rain, start_date=np.datetime64('2000-01-01'), - end_date=np.datetime64('2002-12-31'), domain='atmos', - intvl_in='monthly', dtype_in_vert='sigma', dtype_in_time='ts', - intvl_out=None) - time_bounds = np.array([[0, 31], [31, 59], [59, 90]]) - bounds = np.array([0, 1]) - time = np.array([15, 46, 74]) - data = np.zeros((3, 1, 1)) - lat = [0] - lon = [0] - self.ALT_LAT_STR = 'LATITUDE' - self.var_name = 'a' - ds = xr.DataArray(data, - coords=[time, lat, lon], - dims=[TIME_STR, self.ALT_LAT_STR, LON_STR], - name=self.var_name).to_dataset() - ds[TIME_BOUNDS_STR] = xr.DataArray(time_bounds, - coords=[time, bounds], - dims=[TIME_STR, BOUNDS_STR], - name=TIME_BOUNDS_STR) - units_str = 'days since 2000-01-01 00:00:00' - ds[TIME_STR].attrs['units'] = units_str - ds[TIME_BOUNDS_STR].attrs['units'] = units_str - self.ds = ds - - inst_time = np.array([3, 6, 9]) - inst_units_str = 'hours since 2000-01-01 00:00:00' - inst_ds = ds.copy() - inst_ds.drop(TIME_BOUNDS_STR) - inst_ds[TIME_STR].values = inst_time - inst_ds[TIME_STR].attrs['units'] = inst_units_str - self.inst_ds = inst_ds - - def tearDown(self): - pass - - -class TestDataLoader(AospyDataLoaderTestCase): - def test_rename_grid_attrs_ds(self): - assert LAT_STR not in self.ds - assert self.ALT_LAT_STR in self.ds - ds = grid_attrs_to_aospy_names(self.ds) - assert LAT_STR in ds - - def test_rename_grid_attrs_dim_no_coord(self): - bounds_dim = 'nv' - assert bounds_dim not in self.ds - assert bounds_dim in GRID_ATTRS[BOUNDS_STR] - # Create DataArray with all dims lacking coords - values = self.ds[self.var_name].values - arr = xr.DataArray(values, name='dummy') - # Insert name to be replaced (its physical meaning doesn't matter here) - ds = arr.rename({'dim_0': bounds_dim}).to_dataset() - assert not ds[bounds_dim].coords - result = grid_attrs_to_aospy_names(ds) - assert not result[BOUNDS_STR].coords - - def test_rename_grid_attrs_skip_scalar_dim(self): - phalf_dim = 'phalf' - assert phalf_dim not in self.ds - assert phalf_dim in GRID_ATTRS[PHALF_STR] - ds = self.ds.copy() - ds[phalf_dim] = 4 - ds = ds.set_coords(phalf_dim) - result = grid_attrs_to_aospy_names(ds) - xr.testing.assert_identical(result[phalf_dim], ds[phalf_dim]) - - def test_rename_grid_attrs_copy_attrs(self): - orig_attrs = {'dummy_key': 'dummy_val'} - ds_orig = self.ds.copy() - ds_orig[self.ALT_LAT_STR].attrs = orig_attrs - ds = grid_attrs_to_aospy_names(ds_orig) - self.assertEqual(ds[LAT_STR].attrs, orig_attrs) - - def test_set_grid_attrs_as_coords(self): - ds = grid_attrs_to_aospy_names(self.ds) - sfc_area = ds[self.var_name].isel(**{TIME_STR: 0}).drop(TIME_STR) - ds[SFC_AREA_STR] = sfc_area - - assert SFC_AREA_STR not in ds.coords - - ds = set_grid_attrs_as_coords(ds) - assert SFC_AREA_STR in ds.coords - assert TIME_BOUNDS_STR in ds.coords - - def test_sel_var(self): - time = np.array([0, 31, 59]) + 15 - data = np.zeros((3)) - ds = xr.DataArray(data, - coords=[time], - dims=[TIME_STR], - name=convection_rain.name).to_dataset() - condensation_rain_alt_name, = condensation_rain.alt_names - ds[condensation_rain_alt_name] = xr.DataArray(data, coords=[ds.time]) - result = _sel_var(ds, convection_rain) - self.assertEqual(result.name, convection_rain.name) - - result = _sel_var(ds, condensation_rain) - self.assertEqual(result.name, condensation_rain.name) - - with self.assertRaises(LookupError): - _sel_var(ds, precip) - - def test_maybe_apply_time_shift(self): - ds = xr.decode_cf(self.ds) - da = ds[self.var_name] - - result = self.DataLoader._maybe_apply_time_shift( - da.copy(), **self.generate_file_set_args)[TIME_STR] - assert result.identical(da[TIME_STR]) - - offset = self.DataLoader._maybe_apply_time_shift( - da.copy(), {'days': 1}, **self.generate_file_set_args) - result = offset[TIME_STR] - - expected = da[TIME_STR] + np.timedelta64(1, 'D') - expected[TIME_STR] = expected - - assert result.identical(expected) - - def test_generate_file_set(self): - with self.assertRaises(NotImplementedError): - self.DataLoader._generate_file_set() - - def test_prep_time_data(self): - assert (TIME_WEIGHTS_STR not in self.inst_ds) - ds = _prep_time_data(self.inst_ds) - assert (TIME_WEIGHTS_STR in ds) - - def test_preprocess_and_rename_grid_attrs(self): - def preprocess_func(ds, **kwargs): - # Corrupt a grid attribute name so that we test - # that grid_attrs_to_aospy_names is still called - # after - ds = ds.rename({LON_STR: 'LONGITUDE'}) - ds.attrs['a'] = 'b' - return ds - - assert LAT_STR not in self.ds - assert self.ALT_LAT_STR in self.ds - assert LON_STR in self.ds - - expected = self.ds.rename({self.ALT_LAT_STR: LAT_STR}) - expected = expected.set_coords(TIME_BOUNDS_STR) - expected.attrs['a'] = 'b' - result = _preprocess_and_rename_grid_attrs(preprocess_func)(self.ds) - xr.testing.assert_identical(result, expected) - - -class TestDictDataLoader(TestDataLoader): - def setUp(self): - super(TestDictDataLoader, self).setUp() - file_map = {'monthly': ['a.nc']} - self.DataLoader = DictDataLoader(file_map) - - def test_generate_file_set(self): - result = self.DataLoader._generate_file_set( - **self.generate_file_set_args) - expected = ['a.nc'] - self.assertEqual(result, expected) +_DATE_RANGES = { + 'datetime': (datetime.datetime(2000, 1, 1), + datetime.datetime(2002, 12, 31)), + 'datetime64': (np.datetime64('2000-01-01'), + np.datetime64('2002-12-31')), + 'cftime': (DatetimeNoLeap(2000, 1, 1), + DatetimeNoLeap(2002, 12, 31)), + 'str': ('2000', '2002') +} + + +@pytest.fixture(params=_DATE_RANGES.values(), ids=list(_DATE_RANGES.keys())) +def generate_file_set_args(request): + start_date, end_date = request.param + return dict( + var=condensation_rain, start_date=start_date, end_date=end_date, + domain='atmos', intvl_in='monthly', dtype_in_vert='sigma', + dtype_in_time='ts', intvl_out=None) + + +@pytest.fixture() +def alt_lat_str(): + return 'LATITUDE' + + +@pytest.fixture() +def var_name(): + return 'a' + + +@pytest.fixture() +def ds(alt_lat_str, var_name): + time_bounds = np.array([[0, 31], [31, 59], [59, 90]]) + bounds = np.array([0, 1]) + time = np.array([15, 46, 74]) + data = np.zeros((3, 1, 1)) + lat = [0] + lon = [0] + ds = xr.DataArray(data, + coords=[time, lat, lon], + dims=[TIME_STR, alt_lat_str, LON_STR], + name=var_name).to_dataset() + ds[TIME_BOUNDS_STR] = xr.DataArray(time_bounds, + coords=[time, bounds], + dims=[TIME_STR, BOUNDS_STR], + name=TIME_BOUNDS_STR) + units_str = 'days since 2000-01-01 00:00:00' + ds[TIME_STR].attrs['units'] = units_str + ds[TIME_BOUNDS_STR].attrs['units'] = units_str + return ds + + +@pytest.fixture() +def inst_ds(ds): + inst_time = np.array([3, 6, 9]) + inst_units_str = 'hours since 2000-01-01 00:00:00' + inst_ds = ds.copy() + inst_ds.drop(TIME_BOUNDS_STR) + inst_ds[TIME_STR].values = inst_time + inst_ds[TIME_STR].attrs['units'] = inst_units_str + return inst_ds + + +def _gfdl_data_loader_kwargs(data_start_date, data_end_date): + return dict(data_direc=os.path.join('.', 'test'), + data_dur=6, + data_start_date=data_start_date, + data_end_date=data_end_date, + upcast_float32=False, + data_vars='minimal', + coords='minimal') + + +_DATA_LOADER_KWARGS = { + 'DataLoader': (DataLoader, {}), + 'DictDataLoader': (DictDataLoader, dict(file_map={'monthly': ['a.nc']})), + 'NestedDictDataLoader': ( + NestedDictDataLoader, + dict(file_map={'monthly': {'condensation_rain': ['a.nc']}})), + 'GFDLDataLoader-datetime': ( + GFDLDataLoader, _gfdl_data_loader_kwargs( + datetime.datetime(2000, 1, 1), datetime.datetime(2012, 12, 31))), + 'GFDLDataLoader-datetime64': ( + GFDLDataLoader, _gfdl_data_loader_kwargs( + np.datetime64('2000-01-01'), np.datetime64('2012-12-31'))), + 'GFDLDataLoader-cftime': ( + GFDLDataLoader, _gfdl_data_loader_kwargs( + DatetimeNoLeap(2000, 1, 1), DatetimeNoLeap(2012, 12, 31))), + 'GFDLDataLoader-str': ( + GFDLDataLoader, _gfdl_data_loader_kwargs('2000', '2012')) +} + + +@pytest.fixture(params=_DATA_LOADER_KWARGS.values(), + ids=list(_DATA_LOADER_KWARGS.keys())) +def data_loader(request): + data_loader_type, kwargs = request.param + return data_loader_type(**kwargs) + + +_GFDL_DATA_LOADER_KWARGS = {key: _DATA_LOADER_KWARGS[key] for key in + _DATA_LOADER_KWARGS if 'GFDL' in key} + + +@pytest.fixture(params=_GFDL_DATA_LOADER_KWARGS.values(), + ids=list(_GFDL_DATA_LOADER_KWARGS.keys())) +def gfdl_data_loader(request): + data_loader_type, kwargs = request.param + return data_loader_type(**kwargs) + + +def test_rename_grid_attrs_ds(ds, alt_lat_str): + assert LAT_STR not in ds + assert alt_lat_str in ds + ds = grid_attrs_to_aospy_names(ds) + assert LAT_STR in ds + + +def test_rename_grid_attrs_dim_no_coord(ds, var_name): + bounds_dim = 'nv' + assert bounds_dim not in ds + assert bounds_dim in GRID_ATTRS[BOUNDS_STR] + # Create DataArray with all dims lacking coords + values = ds[var_name].values + arr = xr.DataArray(values, name='dummy') + # Insert name to be replaced (its physical meaning doesn't matter here) + ds = arr.rename({'dim_0': bounds_dim}).to_dataset() + assert not ds[bounds_dim].coords + result = grid_attrs_to_aospy_names(ds) + assert not result[BOUNDS_STR].coords + + +def test_rename_grid_attrs_skip_scalar_dim(ds): + phalf_dim = 'phalf' + assert phalf_dim not in ds + assert phalf_dim in GRID_ATTRS[PHALF_STR] + ds_copy = ds.copy() + ds_copy[phalf_dim] = 4 + ds_copy = ds_copy.set_coords(phalf_dim) + result = grid_attrs_to_aospy_names(ds_copy) + xr.testing.assert_identical(result[phalf_dim], ds_copy[phalf_dim]) + + +def test_rename_grid_attrs_copy_attrs(ds, alt_lat_str): + orig_attrs = {'dummy_key': 'dummy_val'} + ds_orig = ds.copy() + ds_orig[alt_lat_str].attrs = orig_attrs + ds = grid_attrs_to_aospy_names(ds_orig) + assert ds[LAT_STR].attrs == orig_attrs + + +def test_set_grid_attrs_as_coords(ds, var_name): + ds = grid_attrs_to_aospy_names(ds) + sfc_area = ds[var_name].isel(**{TIME_STR: 0}).drop(TIME_STR) + ds[SFC_AREA_STR] = sfc_area + + assert SFC_AREA_STR not in ds.coords + + ds = set_grid_attrs_as_coords(ds) + assert SFC_AREA_STR in ds.coords + assert TIME_BOUNDS_STR in ds.coords + + +def test_sel_var(): + time = np.array([0, 31, 59]) + 15 + data = np.zeros((3)) + ds = xr.DataArray(data, + coords=[time], + dims=[TIME_STR], + name=convection_rain.name).to_dataset() + condensation_rain_alt_name, = condensation_rain.alt_names + ds[condensation_rain_alt_name] = xr.DataArray(data, coords=[ds.time]) + result = _sel_var(ds, convection_rain) + assert result.name == convection_rain.name + + result = _sel_var(ds, condensation_rain) + assert result.name == condensation_rain.name + + with pytest.raises(LookupError): + _sel_var(ds, precip) + + +def test_maybe_apply_time_shift(data_loader, ds, inst_ds, var_name, + generate_file_set_args): + ds = xr.decode_cf(ds) + da = ds[var_name] + + result = data_loader._maybe_apply_time_shift( + da.copy(), **generate_file_set_args)[TIME_STR] + assert result.identical(da[TIME_STR]) + + offset = data_loader._maybe_apply_time_shift( + da.copy(), {'days': 1}, **generate_file_set_args) + result = offset[TIME_STR] + + expected = da[TIME_STR] + np.timedelta64(1, 'D') + expected[TIME_STR] = expected - with self.assertRaises(KeyError): - self.generate_file_set_args['intvl_in'] = 'daily' - result = self.DataLoader._generate_file_set( - **self.generate_file_set_args) + assert result.identical(expected) + + +def test_maybe_apply_time_shift_ts(gfdl_data_loader, ds, var_name, + generate_file_set_args): + ds = xr.decode_cf(ds) + da = ds[var_name] + result = gfdl_data_loader._maybe_apply_time_shift( + da.copy(), **generate_file_set_args)[TIME_STR] + assert result.identical(da[TIME_STR]) + + +def test_maybe_apply_time_shift_inst(gfdl_data_loader, inst_ds, var_name, + generate_file_set_args): + inst_ds = xr.decode_cf(inst_ds) + generate_file_set_args['dtype_in_time'] = 'inst' + generate_file_set_args['intvl_in'] = '3hr' + da = inst_ds[var_name] + result = gfdl_data_loader._maybe_apply_time_shift( + da.copy(), **generate_file_set_args)[TIME_STR] + + expected = da[TIME_STR] + np.timedelta64(-3, 'h') + expected[TIME_STR] = expected + assert result.identical(expected) + + generate_file_set_args['intvl_in'] = 'daily' + da = inst_ds[var_name] + result = gfdl_data_loader._maybe_apply_time_shift( + da.copy(), **generate_file_set_args)[TIME_STR] + + expected = da[TIME_STR] + expected[TIME_STR] = expected + assert result.identical(expected) + + +def test_prep_time_data(inst_ds): + assert (TIME_WEIGHTS_STR not in inst_ds) + ds = _prep_time_data(inst_ds) + assert (TIME_WEIGHTS_STR in ds) -class TestNestedDictDataLoader(TestDataLoader): - def setUp(self): - super(TestNestedDictDataLoader, self).setUp() - file_map = {'monthly': {'condensation_rain': ['a.nc']}} - self.DataLoader = NestedDictDataLoader(file_map) +def test_preprocess_and_rename_grid_attrs(ds, alt_lat_str): + def preprocess_func(ds, **kwargs): + # Corrupt a grid attribute name so that we test + # that grid_attrs_to_aospy_names is still called + # after + ds = ds.rename({LON_STR: 'LONGITUDE'}) + ds.attrs['a'] = 'b' + return ds - def test_generate_file_set(self): - result = self.DataLoader._generate_file_set( - **self.generate_file_set_args) + assert LAT_STR not in ds + assert alt_lat_str in ds + assert LON_STR in ds + + expected = ds.rename({alt_lat_str: LAT_STR}) + expected = expected.set_coords(TIME_BOUNDS_STR) + expected.attrs['a'] = 'b' + result = _preprocess_and_rename_grid_attrs(preprocess_func)(ds) + xr.testing.assert_identical(result, expected) + + +def test_generate_file_set(data_loader, generate_file_set_args): + if type(data_loader) is DataLoader: + with pytest.raises(NotImplementedError): + data_loader._generate_file_set() + + elif isinstance(data_loader, DictDataLoader): + result = data_loader._generate_file_set( + **generate_file_set_args) expected = ['a.nc'] - self.assertEqual(result, expected) - - with self.assertRaises(KeyError): - self.generate_file_set_args['var'] = convection_rain - result = self.DataLoader._generate_file_set( - **self.generate_file_set_args) - - -class TestGFDLDataLoader(TestDataLoader): - def setUp(self): - super(TestGFDLDataLoader, self).setUp() - self.DataLoader = GFDLDataLoader( - data_direc=os.path.join('.', 'test'), - data_dur=6, - data_start_date=datetime.datetime(2000, 1, 1), - data_end_date=datetime.datetime(2012, 12, 31), - upcast_float32=False, - data_vars='minimal', - coords='minimal' - ) - - def test_overriding_constructor(self): - new = GFDLDataLoader(self.DataLoader, - data_direc=os.path.join('.', 'a')) - self.assertEqual(new.data_direc, os.path.join('.', 'a')) - self.assertEqual(new.data_dur, self.DataLoader.data_dur) - self.assertEqual(new.data_start_date, self.DataLoader.data_start_date) - self.assertEqual(new.data_end_date, self.DataLoader.data_end_date) - self.assertEqual(new.preprocess_func, - self.DataLoader.preprocess_func) - self.assertEqual(new.upcast_float32, self.DataLoader.upcast_float32) - - new = GFDLDataLoader(self.DataLoader, data_dur=8) - self.assertEqual(new.data_dur, 8) - - new = GFDLDataLoader(self.DataLoader, - data_start_date=datetime.datetime(2001, 1, 1)) - self.assertEqual(new.data_start_date, datetime.datetime(2001, 1, 1)) - - new = GFDLDataLoader(self.DataLoader, - data_end_date=datetime.datetime(2003, 12, 31)) - self.assertEqual(new.data_end_date, datetime.datetime(2003, 12, 31)) - - new = GFDLDataLoader(self.DataLoader, - preprocess_func=lambda ds: ds) - xr.testing.assert_identical(new.preprocess_func(self.ds), self.ds) - - new = GFDLDataLoader(self.DataLoader, upcast_float32=True) - self.assertEqual(new.upcast_float32, True) - - new = GFDLDataLoader(self.DataLoader, data_vars='all') - self.assertEqual(new.data_vars, 'all') - - new = GFDLDataLoader(self.DataLoader, coords='all') - self.assertEqual(new.coords, 'all') - - def test_maybe_apply_time_offset_inst(self): - inst_ds = xr.decode_cf(self.inst_ds) - self.generate_file_set_args['dtype_in_time'] = 'inst' - self.generate_file_set_args['intvl_in'] = '3hr' - da = inst_ds[self.var_name] - result = self.DataLoader._maybe_apply_time_shift( - da.copy(), **self.generate_file_set_args)[TIME_STR] - - expected = da[TIME_STR] + np.timedelta64(-3, 'h') - expected[TIME_STR] = expected - assert result.identical(expected) - - self.generate_file_set_args['intvl_in'] = 'daily' - da = inst_ds[self.var_name] - result = self.DataLoader._maybe_apply_time_shift( - da.copy(), **self.generate_file_set_args)[TIME_STR] - - expected = da[TIME_STR] - expected[TIME_STR] = expected - assert result.identical(expected) - - def test_maybe_apply_time_offset_ts(self): - ds = xr.decode_cf(self.ds) - da = ds[self.var_name] - - result = self.DataLoader._maybe_apply_time_shift( - da.copy(), **self.generate_file_set_args)[TIME_STR] - assert result.identical(da[TIME_STR]) - - def test_generate_file_set(self): - with self.assertRaises(IOError): - self.DataLoader._generate_file_set(**self.generate_file_set_args) - - def test_input_data_paths_gfdl(self): - expected = [os.path.join('.', 'test', 'atmos', 'ts', 'monthly', '6yr', - 'atmos.200601-201112.temp.nc')] - result = self.DataLoader._input_data_paths_gfdl( - 'temp', np.datetime64('2010-01-01'), - np.datetime64('2010-12-31'), 'atmos', - 'monthly', 'pressure', 'ts', None) - self.assertEqual(result, expected) - - expected = [os.path.join('.', 'test', 'atmos_daily', 'ts', 'daily', - '6yr', - 'atmos_daily.20060101-20111231.temp.nc')] - result = self.DataLoader._input_data_paths_gfdl( - 'temp', datetime.datetime(2010, 1, 1), - datetime.datetime(2010, 12, 31), 'atmos', - 'daily', 'pressure', 'ts', None) - self.assertEqual(result, expected) - - expected = [os.path.join('.', 'test', 'atmos_daily', 'ts', 'daily', - '6yr', - 'atmos_daily.20060101-20111231.temp.nc')] - result = self.DataLoader._input_data_paths_gfdl( - 'temp', '2010', '2010', 'atmos', - 'daily', 'pressure', 'ts', None) - self.assertEqual(result, expected) - - expected = [os.path.join('.', 'test', 'atmos_level', 'ts', 'monthly', - '6yr', 'atmos_level.200601-201112.temp.nc')] - result = self.DataLoader._input_data_paths_gfdl( - 'temp', DatetimeNoLeap(2010, 1, 1), - DatetimeNoLeap(2010, 12, 31), 'atmos', - 'monthly', ETA_STR, 'ts', None) - self.assertEqual(result, expected) - - expected = [os.path.join('.', 'test', 'atmos', 'ts', 'monthly', - '6yr', 'atmos.200601-201112.ps.nc')] - result = self.DataLoader._input_data_paths_gfdl( - 'ps', np.datetime64('2010-01-01'), - np.datetime64('2010-12-31'), 'atmos', - 'monthly', ETA_STR, 'ts', None) - self.assertEqual(result, expected) - - expected = [os.path.join('.', 'test', 'atmos_inst', 'ts', 'monthly', - '6yr', 'atmos_inst.200601-201112.temp.nc')] - result = self.DataLoader._input_data_paths_gfdl( - 'temp', np.datetime64('2010-01-01'), - np.datetime64('2010-12-31'), 'atmos', - 'monthly', 'pressure', 'inst', None) - self.assertEqual(result, expected) - - expected = [os.path.join('.', 'test', 'atmos', 'av', 'monthly_6yr', - 'atmos.2006-2011.jja.nc')] - result = self.DataLoader._input_data_paths_gfdl( - 'temp', np.datetime64('2010-01-01'), - np.datetime64('2010-12-31'), 'atmos', - 'monthly', 'pressure', 'av', 'jja') - self.assertEqual(result, expected) - - def test_data_name_gfdl_annual(self): - for data_type in ['ts', 'inst']: - expected = 'atmos.2010.temp.nc' - result = io.data_name_gfdl('temp', 'atmos', data_type, - 'annual', 2010, None, 2000, 1) - self.assertEqual(result, expected) - - expected = 'atmos.2006-2011.temp.nc' - result = io.data_name_gfdl('temp', 'atmos', data_type, - 'annual', 2010, None, 2000, 6) - self.assertEqual(result, expected) - - for intvl_type in ['annual', 'ann']: - expected = 'atmos.2010.ann.nc' - result = io.data_name_gfdl('temp', 'atmos', 'av', - intvl_type, 2010, None, 2000, 1) - self.assertEqual(result, expected) - - expected = 'atmos.2006-2011.ann.nc' - result = io.data_name_gfdl('temp', 'atmos', 'av', - intvl_type, 2010, None, 2000, 6) - self.assertEqual(result, expected) - - expected = 'atmos.2006-2011.01-12.nc' - result = io.data_name_gfdl('temp', 'atmos', 'av_ts', + result == expected + + with pytest.raises(KeyError): + generate_file_set_args['intvl_in'] = 'daily' + result = data_loader._generate_file_set( + **generate_file_set_args) + + elif isinstance(data_loader, NestedDictDataLoader): + result = data_loader._generate_file_set( + **generate_file_set_args) + expected = ['a.nc'] + assert result == expected + + with pytest.raises(KeyError): + generate_file_set_args['var'] = convection_rain + result = data_loader._generate_file_set( + **generate_file_set_args) + + else: + with pytest.raises(IOError): + data_loader._generate_file_set(**generate_file_set_args) + + +def test_overriding_constructor(gfdl_data_loader, ds): + new = GFDLDataLoader(gfdl_data_loader, + data_direc=os.path.join('.', 'a')) + assert new.data_direc == os.path.join('.', 'a') + assert new.data_dur == gfdl_data_loader.data_dur + assert new.data_start_date == gfdl_data_loader.data_start_date + assert new.data_end_date == gfdl_data_loader.data_end_date + assert new.preprocess_func == gfdl_data_loader.preprocess_func + assert new.upcast_float32 == gfdl_data_loader.upcast_float32 + + new = GFDLDataLoader(gfdl_data_loader, data_dur=8) + assert new.data_dur == 8 + + new = GFDLDataLoader(gfdl_data_loader, + data_start_date=datetime.datetime(2001, 1, 1)) + assert new.data_start_date == datetime.datetime(2001, 1, 1) + + new = GFDLDataLoader(gfdl_data_loader, + data_end_date=datetime.datetime(2003, 12, 31)) + assert new.data_end_date == datetime.datetime(2003, 12, 31) + + new = GFDLDataLoader(gfdl_data_loader, preprocess_func=lambda ds: ds) + xr.testing.assert_identical(new.preprocess_func(ds), ds) + + new = GFDLDataLoader(gfdl_data_loader, upcast_float32=True) + assert new.upcast_float32 + + new = GFDLDataLoader(gfdl_data_loader, data_vars='all') + assert new.data_vars == 'all' + + new = GFDLDataLoader(gfdl_data_loader, coords='all') + assert new.coords == 'all' + + +_GFDL_DATE_RANGES = { + 'datetime': (datetime.datetime(2010, 1, 1), + datetime.datetime(2010, 12, 31)), + 'datetime64': (np.datetime64('2010-01-01'), + np.datetime64('2010-12-31')), + 'cftime': (DatetimeNoLeap(2010, 1, 1), + DatetimeNoLeap(2010, 12, 31)), + 'str': ('2010', '2010') +} + + +@pytest.mark.parametrize(['start_date', 'end_date'], + _GFDL_DATE_RANGES.values(), + ids=list(_GFDL_DATE_RANGES.keys())) +def test_input_data_paths_gfdl(gfdl_data_loader, start_date, end_date): + expected = [os.path.join('.', 'test', 'atmos', 'ts', 'monthly', '6yr', + 'atmos.200601-201112.temp.nc')] + result = gfdl_data_loader._input_data_paths_gfdl( + 'temp', start_date, end_date, 'atmos', + 'monthly', 'pressure', 'ts', None) + assert result == expected + + expected = [os.path.join('.', 'test', 'atmos_daily', 'ts', 'daily', + '6yr', + 'atmos_daily.20060101-20111231.temp.nc')] + result = gfdl_data_loader._input_data_paths_gfdl( + 'temp', start_date, end_date, 'atmos', + 'daily', 'pressure', 'ts', None) + assert result == expected + + expected = [os.path.join('.', 'test', 'atmos_daily', 'ts', 'daily', + '6yr', + 'atmos_daily.20060101-20111231.temp.nc')] + result = gfdl_data_loader._input_data_paths_gfdl( + 'temp', start_date, end_date, 'atmos', + 'daily', 'pressure', 'ts', None) + assert result == expected + + expected = [os.path.join('.', 'test', 'atmos_level', 'ts', 'monthly', + '6yr', 'atmos_level.200601-201112.temp.nc')] + result = gfdl_data_loader._input_data_paths_gfdl( + 'temp', start_date, end_date, 'atmos', + 'monthly', ETA_STR, 'ts', None) + assert result == expected + + expected = [os.path.join('.', 'test', 'atmos', 'ts', 'monthly', + '6yr', 'atmos.200601-201112.ps.nc')] + result = gfdl_data_loader._input_data_paths_gfdl( + 'ps', start_date, end_date, 'atmos', + 'monthly', ETA_STR, 'ts', None) + assert result == expected + + expected = [os.path.join('.', 'test', 'atmos_inst', 'ts', 'monthly', + '6yr', 'atmos_inst.200601-201112.temp.nc')] + result = gfdl_data_loader._input_data_paths_gfdl( + 'temp', start_date, end_date, 'atmos', + 'monthly', 'pressure', 'inst', None) + assert result == expected + + expected = [os.path.join('.', 'test', 'atmos', 'av', 'monthly_6yr', + 'atmos.2006-2011.jja.nc')] + result = gfdl_data_loader._input_data_paths_gfdl( + 'temp', start_date, end_date, 'atmos', + 'monthly', 'pressure', 'av', 'jja') + assert result == expected + + +# TODO: Parametrize these tests +def test_data_name_gfdl_annual(): + for data_type in ['ts', 'inst']: + expected = 'atmos.2010.temp.nc' + result = io.data_name_gfdl('temp', 'atmos', data_type, + 'annual', 2010, None, 2000, 1) + assert result == expected + + expected = 'atmos.2006-2011.temp.nc' + result = io.data_name_gfdl('temp', 'atmos', data_type, 'annual', 2010, None, 2000, 6) - self.assertEqual(result, expected) - - def test_data_name_gfdl_monthly(self): - for data_type in ['ts', 'inst']: - expected = 'atmos.200601-201112.temp.nc' - result = io.data_name_gfdl('temp', 'atmos', data_type, - 'monthly', 2010, 'jja', 2000, 6) - self.assertEqual(result, expected) - - for intvl_type in ['monthly', 'mon']: - expected = 'atmos.2010.jja.nc' - result = io.data_name_gfdl('temp', 'atmos', 'av', - intvl_type, 2010, 'jja', 2000, 1) - self.assertEqual(result, expected) - - expected = 'atmos.2006-2011.jja.nc' - result = io.data_name_gfdl('temp', 'atmos', 'av', - intvl_type, 2010, 'jja', 2000, 6) - self.assertEqual(result, expected) - - expected = 'atmos.2006-2011.01-12.nc' - result = io.data_name_gfdl('temp', 'atmos', 'av_ts', + assert result == expected + + for intvl_type in ['annual', 'ann']: + expected = 'atmos.2010.ann.nc' + result = io.data_name_gfdl('temp', 'atmos', 'av', + intvl_type, 2010, None, 2000, 1) + assert result == expected + + expected = 'atmos.2006-2011.ann.nc' + result = io.data_name_gfdl('temp', 'atmos', 'av', + intvl_type, 2010, None, 2000, 6) + assert result == expected + + expected = 'atmos.2006-2011.01-12.nc' + result = io.data_name_gfdl('temp', 'atmos', 'av_ts', + 'annual', 2010, None, 2000, 6) + assert result == expected + + +def test_data_name_gfdl_monthly(): + for data_type in ['ts', 'inst']: + expected = 'atmos.200601-201112.temp.nc' + result = io.data_name_gfdl('temp', 'atmos', data_type, 'monthly', 2010, 'jja', 2000, 6) - self.assertEqual(result, expected) + assert result == expected - def test_data_name_gfdl_daily(self): - for data_type in ['ts', 'inst']: - expected = 'atmos.20060101-20111231.temp.nc' - result = io.data_name_gfdl('temp', 'atmos', data_type, - 'daily', 2010, None, 2000, 6) - self.assertEqual(result, expected) + for intvl_type in ['monthly', 'mon']: + expected = 'atmos.2010.jja.nc' + result = io.data_name_gfdl('temp', 'atmos', 'av', + intvl_type, 2010, 'jja', 2000, 1) + assert result == expected - with self.assertRaises(NameError): - io.data_name_gfdl('temp', 'atmos', 'av', - 'daily', 2010, None, 2000, 6) + expected = 'atmos.2006-2011.jja.nc' + result = io.data_name_gfdl('temp', 'atmos', 'av', + intvl_type, 2010, 'jja', 2000, 6) + assert result == expected - expected = 'atmos.2006-2011.01-12.nc' - result = io.data_name_gfdl('temp', 'atmos', 'av_ts', - 'daily', 2010, None, 2000, 6) - self.assertEqual(result, expected) + expected = 'atmos.2006-2011.01-12.nc' + result = io.data_name_gfdl('temp', 'atmos', 'av_ts', + 'monthly', 2010, 'jja', 2000, 6) + assert result == expected - def test_data_name_gfdl_hr(self): - for data_type in ['ts', 'inst']: - expected = 'atmos.2006010100-2011123123.temp.nc' - result = io.data_name_gfdl('temp', 'atmos', data_type, - '3hr', 2010, None, 2000, 6) - self.assertEqual(result, expected) - with self.assertRaises(NameError): - io.data_name_gfdl('temp', 'atmos', 'av', - '3hr', 2010, None, 2000, 6) +def test_data_name_gfdl_daily(): + for data_type in ['ts', 'inst']: + expected = 'atmos.20060101-20111231.temp.nc' + result = io.data_name_gfdl('temp', 'atmos', data_type, + 'daily', 2010, None, 2000, 6) + assert result == expected - expected = 'atmos.2006-2011.01-12.nc' - result = io.data_name_gfdl('temp', 'atmos', 'av_ts', - '3hr', 2010, None, 2000, 6) - self.assertEqual(result, expected) - - def test_data_name_gfdl_seasonal(self): - for data_type in ['ts', 'inst']: - with self.assertRaises(NameError): - io.data_name_gfdl('temp', 'atmos', data_type, - 'seasonal', 2010, None, 2000, 6) - - for intvl_type in ['seasonal', 'seas']: - expected = 'atmos.2010.JJA.nc' - result = io.data_name_gfdl('temp', 'atmos', 'av', - intvl_type, 2010, 'jja', 2000, 1) - self.assertEqual(result, expected) - - expected = 'atmos.2006-2011.JJA.nc' - result = io.data_name_gfdl('temp', 'atmos', 'av', - intvl_type, 2010, 'jja', 2000, 6) - self.assertEqual(result, expected) - - expected = 'atmos.2006-2011.01-12.nc' - result = io.data_name_gfdl('temp', 'atmos', 'av_ts', - 'seasonal', 2010, None, 2000, 6) - self.assertEqual(result, expected) - - -class LoadVariableTestCase(unittest.TestCase): - def setUp(self): - self.data_loader = example_run.data_loader - - def tearDown(self): - # Restore default values of data_vars and coords - self.data_loader.data_vars = 'minimal' - self.data_loader.coords = 'minimal' - self.data_loader.preprocess_func = lambda ds, **kwargs: ds - - def test_load_variable(self): - result = self.data_loader.load_variable( - condensation_rain, DatetimeNoLeap(5, 1, 1), - DatetimeNoLeap(5, 12, 31), - intvl_in='monthly') - filepath = os.path.join(os.path.split(ROOT_PATH)[0], 'netcdf', - '00050101.precip_monthly.nc') - expected = _open_ds_catch_warnings(filepath)['condensation_rain'] - np.testing.assert_array_equal(result.values, expected.values) - - def test_load_variable_datetime_datetime(self): - result = self.data_loader.load_variable( - condensation_rain, datetime.datetime(5, 1, 1), - datetime.datetime(5, 12, 31), - intvl_in='monthly') - filepath = os.path.join(os.path.split(ROOT_PATH)[0], 'netcdf', - '00050101.precip_monthly.nc') - expected = _open_ds_catch_warnings(filepath)['condensation_rain'] - np.testing.assert_array_equal(result.values, expected.values) - - def test_load_variable_datetime64(self): - result = self.data_loader.load_variable( - condensation_rain, np.datetime64('0005-01-01'), - np.datetime64('0005-12-31'), - intvl_in='monthly') - filepath = os.path.join(os.path.split(ROOT_PATH)[0], 'netcdf', - '00050101.precip_monthly.nc') - expected = _open_ds_catch_warnings(filepath)['condensation_rain'] - np.testing.assert_array_equal(result.values, expected.values) - - def test_load_variable_str(self): - result = self.data_loader.load_variable( - condensation_rain, '0005', '0005', intvl_in='monthly') - filepath = os.path.join(os.path.split(ROOT_PATH)[0], 'netcdf', - '00050101.precip_monthly.nc') - expected = _open_ds_catch_warnings(filepath)['condensation_rain'] - np.testing.assert_array_equal(result.values, expected.values) - - def test_load_variable_does_not_warn(self): - with warnings.catch_warnings(record=True) as warnlog: - self.data_loader.load_variable(condensation_rain, - DatetimeNoLeap(5, 1, 1), - DatetimeNoLeap(5, 12, 31), - intvl_in='monthly') - assert len(warnlog) == 0 - - def test_load_variable_float32_to_float64(self): - def preprocess(ds, **kwargs): - # This function converts testing data to the float32 datatype - return ds.astype(np.float32) - self.data_loader.preprocess_func = preprocess - result = self.data_loader.load_variable( - condensation_rain, DatetimeNoLeap(4, 1, 1), - DatetimeNoLeap(4, 12, 31), - intvl_in='monthly').dtype - expected = np.float64 - self.assertEqual(result, expected) - - def test_load_variable_maintain_float32(self): - def preprocess(ds, **kwargs): - # This function converts testing data to the float32 datatype - return ds.astype(np.float32) - self.data_loader.preprocess_func = preprocess - self.data_loader.upcast_float32 = False - result = self.data_loader.load_variable( - condensation_rain, DatetimeNoLeap(4, 1, 1), - DatetimeNoLeap(4, 12, 31), - intvl_in='monthly').dtype - expected = np.float32 - self.assertEqual(result, expected) - - def test_load_variable_data_vars_all(self): - def preprocess(ds, **kwargs): - # This function drops the time coordinate from condensation_rain - temp = ds[condensation_rain.name] - temp = temp.isel(time=0, drop=True) - ds = ds.drop(condensation_rain.name) - ds[condensation_rain.name] = temp - assert TIME_STR not in ds[condensation_rain.name].coords - return ds - - self.data_loader.data_vars = 'all' - self.data_loader.preprocess_func = preprocess - data = self.data_loader.load_variable( - condensation_rain, DatetimeNoLeap(4, 1, 1), - DatetimeNoLeap(5, 12, 31), - intvl_in='monthly') - result = TIME_STR in data.coords - self.assertEqual(result, True) + with pytest.raises(NameError): + io.data_name_gfdl('temp', 'atmos', 'av', + 'daily', 2010, None, 2000, 6) - def test_load_variable_data_vars_default(self): - data = self.data_loader.load_variable( - condensation_rain, DatetimeNoLeap(4, 1, 1), - DatetimeNoLeap(5, 12, 31), - intvl_in='monthly') - result = TIME_STR in data.coords - self.assertEqual(result, True) - - def test_load_variable_coords_all(self): - self.data_loader.coords = 'all' - data = self.data_loader.load_variable( - condensation_rain, DatetimeNoLeap(4, 1, 1), - DatetimeNoLeap(5, 12, 31), - intvl_in='monthly') - result = TIME_STR in data[ZSURF_STR].coords - self.assertEqual(result, True) - - def test_load_variable_non_0001_refdate(self): - def preprocess(ds, **kwargs): - # This function converts our testing data (encoded with a units - # attribute with a reference data of 0001-01-01) to one - # with a reference data of 0004-01-01 (to do so we also need - # to offset the raw time values by three years). - three_yrs = 1095. - ds['time'] = ds['time'] - three_yrs - ds['time'].attrs['units'] = 'days since 0004-01-01 00:00:00' - ds['time'].attrs['calendar'] = 'noleap' - ds['time_bounds'] = ds['time_bounds'] - three_yrs - ds['time_bounds'].attrs['units'] = 'days since 0004-01-01 00:00:00' - ds['time_bounds'].attrs['calendar'] = 'noleap' - return ds - - self.data_loader.preprocess_func = preprocess - - for year in [4, 5, 6]: - result = self.data_loader.load_variable( - condensation_rain, DatetimeNoLeap(year, 1, 1), - DatetimeNoLeap(year, 12, 31), - intvl_in='monthly') - filepath = os.path.join(os.path.split(ROOT_PATH)[0], 'netcdf', - '000{}0101.precip_monthly.nc'.format(year)) - expected = _open_ds_catch_warnings(filepath)['condensation_rain'] - np.testing.assert_allclose(result.values, expected.values) - - def test_load_variable_preprocess(self): - def preprocess(ds, **kwargs): - if kwargs['start_date'] == DatetimeNoLeap(5, 1, 1): - ds['condensation_rain'] = 10. * ds['condensation_rain'] - return ds - - self.data_loader.preprocess_func = preprocess - - result = self.data_loader.load_variable( - condensation_rain, DatetimeNoLeap(5, 1, 1), - DatetimeNoLeap(5, 12, 31), - intvl_in='monthly') - filepath = os.path.join(os.path.split(ROOT_PATH)[0], 'netcdf', - '00050101.precip_monthly.nc') - expected = 10. * _open_ds_catch_warnings(filepath)['condensation_rain'] - np.testing.assert_allclose(result.values, expected.values) - - result = self.data_loader.load_variable( - condensation_rain, DatetimeNoLeap(4, 1, 1), - DatetimeNoLeap(4, 12, 31), - intvl_in='monthly') - filepath = os.path.join(os.path.split(ROOT_PATH)[0], 'netcdf', - '00040101.precip_monthly.nc') - expected = _open_ds_catch_warnings(filepath)['condensation_rain'] - np.testing.assert_allclose(result.values, expected.values) - - def test_load_variable_mask_and_scale(self): - def convert_all_to_missing_val(ds, **kwargs): - ds['condensation_rain'] = 0. * ds['condensation_rain'] + 1.0e20 - ds['condensation_rain'].attrs['_FillValue'] = 1.0e20 - return ds - - self.data_loader.preprocess_func = convert_all_to_missing_val - - data = self.data_loader.load_variable( - condensation_rain, DatetimeNoLeap(5, 1, 1), - DatetimeNoLeap(5, 12, 31), - intvl_in='monthly') + expected = 'atmos.2006-2011.01-12.nc' + result = io.data_name_gfdl('temp', 'atmos', 'av_ts', + 'daily', 2010, None, 2000, 6) + assert result == expected - num_non_missing = np.isfinite(data).sum().item() - expected_num_non_missing = 0 - self.assertEqual(num_non_missing, expected_num_non_missing) - def test_recursively_compute_variable_native(self): - result = self.data_loader.recursively_compute_variable( - condensation_rain, DatetimeNoLeap(5, 1, 1), - DatetimeNoLeap(5, 12, 31), - intvl_in='monthly') - filepath = os.path.join(os.path.split(ROOT_PATH)[0], 'netcdf', - '00050101.precip_monthly.nc') - expected = _open_ds_catch_warnings(filepath)['condensation_rain'] - np.testing.assert_array_equal(result.values, expected.values) - - def test_recursively_compute_variable_one_level(self): - one_level = Var( - name='one_level', variables=(condensation_rain, condensation_rain), - func=lambda x, y: x + y) - result = self.data_loader.recursively_compute_variable( - one_level, DatetimeNoLeap(5, 1, 1), DatetimeNoLeap(5, 12, 31), - intvl_in='monthly') - filepath = os.path.join(os.path.split(ROOT_PATH)[0], 'netcdf', - '00050101.precip_monthly.nc') - expected = 2. * _open_ds_catch_warnings(filepath)['condensation_rain'] - np.testing.assert_array_equal(result.values, expected.values) - - def test_recursively_compute_variable_multi_level(self): - one_level = Var( - name='one_level', variables=(condensation_rain, condensation_rain), - func=lambda x, y: x + y) - multi_level = Var( - name='multi_level', variables=(one_level, condensation_rain), - func=lambda x, y: x + y) - result = self.data_loader.recursively_compute_variable( - multi_level, DatetimeNoLeap(5, 1, 1), DatetimeNoLeap(5, 12, 31), +def test_data_name_gfdl_hr(): + for data_type in ['ts', 'inst']: + expected = 'atmos.2006010100-2011123123.temp.nc' + result = io.data_name_gfdl('temp', 'atmos', data_type, + '3hr', 2010, None, 2000, 6) + assert result == expected + + with pytest.raises(NameError): + io.data_name_gfdl('temp', 'atmos', 'av', + '3hr', 2010, None, 2000, 6) + + expected = 'atmos.2006-2011.01-12.nc' + result = io.data_name_gfdl('temp', 'atmos', 'av_ts', + '3hr', 2010, None, 2000, 6) + assert result == expected + + +def test_data_name_gfdl_seasonal(): + for data_type in ['ts', 'inst']: + with pytest.raises(NameError): + io.data_name_gfdl('temp', 'atmos', data_type, + 'seasonal', 2010, None, 2000, 6) + + for intvl_type in ['seasonal', 'seas']: + expected = 'atmos.2010.JJA.nc' + result = io.data_name_gfdl('temp', 'atmos', 'av', + intvl_type, 2010, 'jja', 2000, 1) + assert result == expected + + expected = 'atmos.2006-2011.JJA.nc' + result = io.data_name_gfdl('temp', 'atmos', 'av', + intvl_type, 2010, 'jja', 2000, 6) + assert result == expected + + expected = 'atmos.2006-2011.01-12.nc' + result = io.data_name_gfdl('temp', 'atmos', 'av_ts', + 'seasonal', 2010, None, 2000, 6) + assert result == expected + + +@pytest.fixture() +def load_variable_data_loader(): + return NestedDictDataLoader(file_map, upcast_float32=False) + + +_LOAD_VAR_DATE_RANGES = { + 'datetime': (datetime.datetime(5, 1, 1), + datetime.datetime(5, 12, 31)), + 'datetime64': (np.datetime64('0005-01-01'), + np.datetime64('0005-12-31')), + 'cftime': (DatetimeNoLeap(5, 1, 1), DatetimeNoLeap(5, 12, 31)), + 'str': ('0005', '0005') +} + + +@pytest.mark.parametrize(['start_date', 'end_date'], + _LOAD_VAR_DATE_RANGES.values(), + ids=list(_LOAD_VAR_DATE_RANGES.keys())) +def test_load_variable(load_variable_data_loader, start_date, end_date): + result = load_variable_data_loader.load_variable( + condensation_rain, start_date, end_date, intvl_in='monthly') + filepath = os.path.join(os.path.split(ROOT_PATH)[0], 'netcdf', + '00050101.precip_monthly.nc') + expected = _open_ds_catch_warnings(filepath)['condensation_rain'] + np.testing.assert_array_equal(result.values, expected.values) + + +@pytest.mark.parametrize(['start_date', 'end_date'], + _LOAD_VAR_DATE_RANGES.values(), + ids=list(_LOAD_VAR_DATE_RANGES.keys())) +def test_load_variable_does_not_warn(load_variable_data_loader, + start_date, end_date): + with warnings.catch_warnings(record=True) as warnlog: + load_variable_data_loader.load_variable( + condensation_rain, + start_date, end_date, intvl_in='monthly') - filepath = os.path.join(os.path.split(ROOT_PATH)[0], 'netcdf', - '00050101.precip_monthly.nc') - expected = 3. * _open_ds_catch_warnings(filepath)['condensation_rain'] - np.testing.assert_array_equal(result.values, expected.values) + assert len(warnlog) == 0 + + +@pytest.mark.parametrize(['start_date', 'end_date'], + _LOAD_VAR_DATE_RANGES.values(), + ids=list(_LOAD_VAR_DATE_RANGES.keys())) +def test_load_variable_float32_to_float64(load_variable_data_loader, + start_date, end_date): + def preprocess(ds, **kwargs): + # This function converts testing data to the float32 datatype + return ds.astype(np.float32) + load_variable_data_loader.upcast_float32 = True + load_variable_data_loader.preprocess_func = preprocess + result = load_variable_data_loader.load_variable( + condensation_rain, start_date, + end_date, + intvl_in='monthly').dtype + expected = np.float64 + assert result == expected + + +@pytest.mark.parametrize(['start_date', 'end_date'], + _LOAD_VAR_DATE_RANGES.values(), + ids=list(_LOAD_VAR_DATE_RANGES.keys())) +def test_load_variable_maintain_float32(load_variable_data_loader, + start_date, end_date): + def preprocess(ds, **kwargs): + # This function converts testing data to the float32 datatype + return ds.astype(np.float32) + load_variable_data_loader.preprocess_func = preprocess + load_variable_data_loader.upcast_float32 = False + result = load_variable_data_loader.load_variable( + condensation_rain, start_date, + end_date, + intvl_in='monthly').dtype + expected = np.float32 + assert result == expected + + +_LOAD_VAR_MULTI_YEAR_RANGES = { + 'datetime': (datetime.datetime(4, 1, 1), + datetime.datetime(5, 12, 31)), + 'datetime64': (np.datetime64('0004-01-01'), + np.datetime64('0005-12-31')), + 'cftime': (DatetimeNoLeap(4, 1, 1), DatetimeNoLeap(5, 12, 31)), + 'str': ('0004', '0005') +} + + +@pytest.mark.parametrize(['start_date', 'end_date'], + _LOAD_VAR_MULTI_YEAR_RANGES.values(), + ids=list(_LOAD_VAR_MULTI_YEAR_RANGES.keys())) +def test_load_variable_data_vars_all(load_variable_data_loader, + start_date, end_date): + def preprocess(ds, **kwargs): + # This function drops the time coordinate from condensation_rain + temp = ds[condensation_rain.name] + temp = temp.isel(time=0, drop=True) + ds = ds.drop(condensation_rain.name) + ds[condensation_rain.name] = temp + assert TIME_STR not in ds[condensation_rain.name].coords + return ds + + load_variable_data_loader.data_vars = 'all' + load_variable_data_loader.preprocess_func = preprocess + data = load_variable_data_loader.load_variable( + condensation_rain, start_date, end_date, + intvl_in='monthly') + assert TIME_STR in data.coords + + +@pytest.mark.parametrize(['start_date', 'end_date'], + _LOAD_VAR_MULTI_YEAR_RANGES.values(), + ids=list(_LOAD_VAR_MULTI_YEAR_RANGES.keys())) +def test_load_variable_data_vars_default(load_variable_data_loader, start_date, + end_date): + data = load_variable_data_loader.load_variable( + condensation_rain, start_date, end_date, + intvl_in='monthly') + assert TIME_STR in data.coords + + +@pytest.mark.parametrize(['start_date', 'end_date'], + _LOAD_VAR_MULTI_YEAR_RANGES.values(), + ids=list(_LOAD_VAR_MULTI_YEAR_RANGES.keys())) +def test_load_variable_coords_all(load_variable_data_loader, start_date, + end_date): + load_variable_data_loader.coords = 'all' + data = load_variable_data_loader.load_variable( + condensation_rain, start_date, end_date, + intvl_in='monthly') + assert TIME_STR in data[ZSURF_STR].coords + + +@pytest.mark.parametrize('year', [4, 5, 6]) +def test_load_variable_non_0001_refdate(load_variable_data_loader, year): + def preprocess(ds, **kwargs): + # This function converts our testing data (encoded with a units + # attribute with a reference data of 0001-01-01) to one + # with a reference data of 0004-01-01 (to do so we also need + # to offset the raw time values by three years). + three_yrs = 1095. + ds['time'] = ds['time'] - three_yrs + ds['time'].attrs['units'] = 'days since 0004-01-01 00:00:00' + ds['time'].attrs['calendar'] = 'noleap' + ds['time_bounds'] = ds['time_bounds'] - three_yrs + ds['time_bounds'].attrs['units'] = 'days since 0004-01-01 00:00:00' + ds['time_bounds'].attrs['calendar'] = 'noleap' + return ds + + load_variable_data_loader.preprocess_func = preprocess + + result = load_variable_data_loader.load_variable( + condensation_rain, DatetimeNoLeap(year, 1, 1), + DatetimeNoLeap(year, 12, 31), + intvl_in='monthly') + filepath = os.path.join(os.path.split(ROOT_PATH)[0], 'netcdf', + '000{}0101.precip_monthly.nc'.format(year)) + expected = _open_ds_catch_warnings(filepath)['condensation_rain'] + np.testing.assert_allclose(result.values, expected.values) + + +def test_load_variable_preprocess(load_variable_data_loader): + def preprocess(ds, **kwargs): + if kwargs['start_date'] == DatetimeNoLeap(5, 1, 1): + ds['condensation_rain'] = 10. * ds['condensation_rain'] + return ds + + load_variable_data_loader.preprocess_func = preprocess + + result = load_variable_data_loader.load_variable( + condensation_rain, DatetimeNoLeap(5, 1, 1), + DatetimeNoLeap(5, 12, 31), + intvl_in='monthly') + filepath = os.path.join(os.path.split(ROOT_PATH)[0], 'netcdf', + '00050101.precip_monthly.nc') + expected = 10. * _open_ds_catch_warnings(filepath)['condensation_rain'] + np.testing.assert_allclose(result.values, expected.values) + + result = load_variable_data_loader.load_variable( + condensation_rain, DatetimeNoLeap(4, 1, 1), + DatetimeNoLeap(4, 12, 31), + intvl_in='monthly') + filepath = os.path.join(os.path.split(ROOT_PATH)[0], 'netcdf', + '00040101.precip_monthly.nc') + expected = _open_ds_catch_warnings(filepath)['condensation_rain'] + np.testing.assert_allclose(result.values, expected.values) + + +def test_load_variable_mask_and_scale(load_variable_data_loader): + def convert_all_to_missing_val(ds, **kwargs): + ds['condensation_rain'] = 0. * ds['condensation_rain'] + 1.0e20 + ds['condensation_rain'].attrs['_FillValue'] = 1.0e20 + return ds + + load_variable_data_loader.preprocess_func = convert_all_to_missing_val + + data = load_variable_data_loader.load_variable( + condensation_rain, DatetimeNoLeap(5, 1, 1), + DatetimeNoLeap(5, 12, 31), + intvl_in='monthly') + + num_non_missing = np.isfinite(data).sum().item() + expected_num_non_missing = 0 + assert num_non_missing == expected_num_non_missing + + +def test_recursively_compute_variable_native(load_variable_data_loader): + result = load_variable_data_loader.recursively_compute_variable( + condensation_rain, DatetimeNoLeap(5, 1, 1), + DatetimeNoLeap(5, 12, 31), + intvl_in='monthly') + filepath = os.path.join(os.path.split(ROOT_PATH)[0], 'netcdf', + '00050101.precip_monthly.nc') + expected = _open_ds_catch_warnings(filepath)['condensation_rain'] + np.testing.assert_array_equal(result.values, expected.values) + + +def test_recursively_compute_variable_one_level(load_variable_data_loader): + one_level = Var( + name='one_level', variables=(condensation_rain, condensation_rain), + func=lambda x, y: x + y) + result = load_variable_data_loader.recursively_compute_variable( + one_level, DatetimeNoLeap(5, 1, 1), DatetimeNoLeap(5, 12, 31), + intvl_in='monthly') + filepath = os.path.join(os.path.split(ROOT_PATH)[0], 'netcdf', + '00050101.precip_monthly.nc') + expected = 2. * _open_ds_catch_warnings(filepath)['condensation_rain'] + np.testing.assert_array_equal(result.values, expected.values) + + +def test_recursively_compute_variable_multi_level(load_variable_data_loader): + one_level = Var( + name='one_level', variables=(condensation_rain, condensation_rain), + func=lambda x, y: x + y) + multi_level = Var( + name='multi_level', variables=(one_level, condensation_rain), + func=lambda x, y: x + y) + result = load_variable_data_loader.recursively_compute_variable( + multi_level, DatetimeNoLeap(5, 1, 1), DatetimeNoLeap(5, 12, 31), + intvl_in='monthly') + filepath = os.path.join(os.path.split(ROOT_PATH)[0], 'netcdf', + '00050101.precip_monthly.nc') + expected = 3. * _open_ds_catch_warnings(filepath)['condensation_rain'] + np.testing.assert_array_equal(result.values, expected.values) if __name__ == '__main__': From 5f8d556b842b2ea62521fd46dbfada51aab5e508 Mon Sep 17 00:00:00 2001 From: Spencer Clark Date: Tue, 29 May 2018 11:09:36 -0400 Subject: [PATCH 15/17] Address review comments --- aospy/data_loader.py | 32 ++++++------ aospy/test/test_utils_times.py | 95 +++++++++++++++++++++++----------- aospy/utils/times.py | 62 ++++++++++++---------- docs/examples.rst | 16 +++--- setup.py | 3 +- 5 files changed, 124 insertions(+), 84 deletions(-) diff --git a/aospy/data_loader.py b/aospy/data_loader.py index 077bb69..719214d 100644 --- a/aospy/data_loader.py +++ b/aospy/data_loader.py @@ -15,7 +15,7 @@ def _preprocess_and_rename_grid_attrs(func, **kwargs): - """Call a custom preprocessing method first then rename grid attrs + """Call a custom preprocessing method first then rename grid attrs. This wrapper is needed to generate a single function to pass to the ``preprocesss`` of xr.open_mfdataset. It makes sure that the @@ -99,7 +99,7 @@ def set_grid_attrs_as_coords(ds): def _maybe_cast_to_float64(da): - """Cast DataArrays to np.float64 if they are of type np.float32 + """Cast DataArrays to np.float64 if they are of type np.float32. Parameters ---------- @@ -156,16 +156,14 @@ def _sel_var(ds, var, upcast_float32=True): def _prep_time_data(ds): - """Prepare time coord. information in Dataset for use in aospy. + """Prepare time coordinate information in Dataset for use in aospy. - 1. Edit units attribute of time variable if it contains a Timestamp invalid - date - 2. If the Dataset contains a time bounds coordinate, add attributes + 1. If the Dataset contains a time bounds coordinate, add attributes representing the true beginning and end dates of the time interval used to construct the Dataset - 3. If the Dataset contains a time bounds coordinate, overwrite the time + 2. If the Dataset contains a time bounds coordinate, overwrite the time coordinate values with the averages of the time bounds at each timestep - 4. Decode the times into np.datetime64 objects for time indexing + 3. Decode the times into np.datetime64 objects for time indexing Parameters ---------- @@ -175,8 +173,8 @@ def _prep_time_data(ds): Returns ------- - Dataset, int, int - The processed Dataset and minimum and maximum years in the loaded data + Dataset + The processed Dataset """ ds = times.ensure_time_as_index(ds) if TIME_BOUNDS_STR in ds: @@ -237,7 +235,7 @@ def apply_preload_user_commands(file_set, cmd=io.dmget): def _setattr_default(obj, attr, value, default): - """Set an attribute of an object to a value or default value""" + """Set an attribute of an object to a value or default value.""" if value is None: setattr(obj, attr, default) else: @@ -245,7 +243,7 @@ def _setattr_default(obj, attr, value, default): class DataLoader(object): - """A fundamental DataLoader object""" + """A fundamental DataLoader object.""" def load_variable(self, var=None, start_date=None, end_date=None, time_offset=None, **DataAttrs): """Load a DataArray for requested variable and time range. @@ -290,7 +288,7 @@ def load_variable(self, var=None, start_date=None, end_date=None, def recursively_compute_variable(self, var, start_date=None, end_date=None, time_offset=None, **DataAttrs): - """Compute a variable recursively, loading data where needed + """Compute a variable recursively, loading data where needed. An obvious requirement here is that the variable must eventually be able to be expressed in terms of model-native quantities; otherwise the @@ -340,7 +338,7 @@ def _generate_file_set(self, var=None, start_date=None, end_date=None, class DictDataLoader(DataLoader): - """A DataLoader that uses a dict mapping lists of files to string tags + """A DataLoader that uses a dict mapping lists of files to string tags. This is the simplest DataLoader; it is useful for instance if one is dealing with raw model history files, which tend to group all variables @@ -389,7 +387,7 @@ class DictDataLoader(DataLoader): """ def __init__(self, file_map=None, upcast_float32=True, data_vars='minimal', coords='minimal', preprocess_func=lambda ds, **kwargs: ds): - """Create a new DictDataLoader""" + """Create a new DictDataLoader.""" self.file_map = file_map self.upcast_float32 = upcast_float32 self.data_vars = data_vars @@ -408,7 +406,7 @@ def _generate_file_set(self, var=None, start_date=None, end_date=None, class NestedDictDataLoader(DataLoader): - """DataLoader that uses a nested dictionary mapping to load files + """DataLoader that uses a nested dictionary mapping to load files. This is the most flexible existing type of DataLoader; it allows for the specification of different sets of files for different variables. The @@ -470,7 +468,7 @@ def _generate_file_set(self, var=None, start_date=None, end_date=None, class GFDLDataLoader(DataLoader): - """DataLoader for NOAA GFDL model output + """DataLoader for NOAA GFDL model output. This is an example of a domain-specific custom DataLoader, designed specifically for finding files output by the Geophysical Fluid Dynamics diff --git a/aospy/test/test_utils_times.py b/aospy/test/test_utils_times.py index 4483220..f42d8b4 100755 --- a/aospy/test/test_utils_times.py +++ b/aospy/test/test_utils_times.py @@ -8,12 +8,15 @@ import pytest import xarray as xr +from itertools import product + from aospy.data_loader import set_grid_attrs_as_coords from aospy.internal_names import ( TIME_STR, TIME_BOUNDS_STR, BOUNDS_STR, TIME_WEIGHTS_STR, RAW_START_DATE_STR, RAW_END_DATE_STR, SUBSET_START_DATE_STR, SUBSET_END_DATE_STR ) +from aospy.automate import _merge_dicts from aospy.utils.times import ( apply_time_offset, average_time_bounds, @@ -315,7 +318,7 @@ def test_assert_has_data_for_time(): _assert_has_data_for_time(da, start_date_bad, end_date_bad) -CFTIME_DATE_TYPES = { +_CFTIME_DATE_TYPES = { 'noleap': cftime.DatetimeNoLeap, '365_day': cftime.DatetimeNoLeap, '360_day': cftime.Datetime360Day, @@ -328,7 +331,7 @@ def test_assert_has_data_for_time(): @pytest.mark.parametrize(['calendar', 'date_type'], - list(CFTIME_DATE_TYPES.items())) + list(_CFTIME_DATE_TYPES.items())) def test_assert_has_data_for_time_cftime_datetimes(calendar, date_type): time_bounds = np.array([[0, 2], [2, 4], [4, 6]]) nv = np.array([0, 1]) @@ -539,49 +542,81 @@ def test_average_time_bounds(ds_time_encoded_cf): xr.testing.assert_identical(actual, desired) +_INFER_YEAR_TESTS = [ + (np.datetime64('2000-01-01'), 2000), + (datetime.datetime(2000, 1, 1), 2000), + ('2000', 2000), + ('2000-01', 2000), + ('2000-01-01', 2000) +] +_INFER_YEAR_TESTS = _INFER_YEAR_TESTS + [ + (date_type(2000, 1, 1), 2000) for date_type in _CFTIME_DATE_TYPES.values()] + + @pytest.mark.parametrize( ['date', 'expected'], - [(np.datetime64('2000-01-01'), 2000), - ('2000-01-01', 2000), - (cftime.DatetimeNoLeap(2000, 1, 1), 2000)]) + _INFER_YEAR_TESTS) def test_infer_year(date, expected): assert infer_year(date) == expected -@pytest.mark.parametrize('date', ['-0001', 'A001']) +@pytest.mark.parametrize('date', ['-0001', 'A001', '01']) def test_infer_year_invalid(date): with pytest.raises(ValueError): infer_year(date) -DATETIME_INDEX = pd.date_range('2000-01-01', freq='M', periods=1) -CFTIME_INDEX = xr.CFTimeIndex([cftime.DatetimeNoLeap(1, 1, 1)]) -CONVERT_DATE_TYPE_TESTS = { - 'DatetimeIndex-np.datetime64': - (DATETIME_INDEX, np.datetime64('2000-01'), np.datetime64('2000-01')), - 'DatetimeIndex-datetime.datetime': - (DATETIME_INDEX, datetime.datetime(2000, 1, 1), np.datetime64('2000-01')), - 'DatetimeIndex-cftime.DatetimeGregorian': - (DATETIME_INDEX, cftime.DatetimeGregorian(2000, 1, 1), - np.datetime64('2000-01')), - 'DatetimeIndex-str': (DATETIME_INDEX, '2000', '2000'), - 'CFTimeIndex[DatetimeNoLeap]-np.datetime64': - (CFTIME_INDEX, np.datetime64('0001-01'), cftime.DatetimeNoLeap(1, 1, 1)), - 'CFTimeIndex[DatetimeNoLeap]-datetime.datetime': - (CFTIME_INDEX, datetime.datetime(1, 1, 1), cftime.DatetimeNoLeap(1, 1, 1)), - 'CFTimeIndex[DatetimeNoLeap]-cftime.DatetimeNoLeap': - (CFTIME_INDEX, cftime.DatetimeNoLeap(1, 1, 1), - cftime.DatetimeNoLeap(1, 1, 1)), - 'CFTimeIndex[DatetimeNoLeap]-cftime.DatetimeGregorian': - (CFTIME_INDEX, cftime.DatetimeGregorian(1, 1, 1), - cftime.DatetimeNoLeap(1, 1, 1)), - 'CFTimeIndex-str': (CFTIME_INDEX, '0001', '0001') +_DATETIME_INDEX = pd.date_range('2000-01-01', freq='M', periods=1) +_DATETIME_CONVERT_TESTS = {} +for date_label, date_type in _CFTIME_DATE_TYPES.items(): + key = 'DatetimeIndex-{}'.format(date_label) + _DATETIME_CONVERT_TESTS[key] = (_DATETIME_INDEX, date_type(2000, 1, 1), + np.datetime64('2000-01')) +_NON_CFTIME_DATES = { + 'datetime.datetime': datetime.datetime(2000, 1, 1), + 'np.datetime64': np.datetime64('2000-01-01'), + 'str': '2000' } +for date_label, date in _NON_CFTIME_DATES.items(): + key = 'DatetimeIndex-{}'.format(date_label) + if isinstance(date, str): + _DATETIME_CONVERT_TESTS[key] = (_DATETIME_INDEX, date, date) + else: + _DATETIME_CONVERT_TESTS[key] = (_DATETIME_INDEX, date, + np.datetime64('2000-01')) + +_CFTIME_INDEXES = { + 'CFTimeIndex[{}]'.format(key): xr.CFTimeIndex([value(1, 1, 1)]) for + key, value in _CFTIME_DATE_TYPES.items() +} +_CFTIME_CONVERT_TESTS = {} +for ((index_label, index), + (date_label, date_type)) in product(_CFTIME_INDEXES.items(), + _CFTIME_DATE_TYPES.items()): + key = '{}-{}'.format(index_label, date_label) + _CFTIME_CONVERT_TESTS[key] = (index, date_type(1, 1, 1), + index.date_type(1, 1, 1)) +_NON_CFTIME_DATES_0001 = { + 'datetime.datetime': datetime.datetime(1, 1, 1), + 'np.datetime64': np.datetime64('0001-01-01'), + 'str': '0001' +} +for ((idx_label, index), + (date_label, date)) in product(_CFTIME_INDEXES.items(), + _NON_CFTIME_DATES_0001.items()): + key = '{}-{}'.format(index_label, date_label) + if isinstance(date, str): + _CFTIME_CONVERT_TESTS[key] = (index, date, date) + else: + _CFTIME_CONVERT_TESTS[key] = (index, date, index.date_type(1, 1, 1)) + +_CONVERT_DATE_TYPE_TESTS = _merge_dicts(_DATETIME_CONVERT_TESTS, + _CFTIME_CONVERT_TESTS) @pytest.mark.parametrize(['index', 'date', 'expected'], - list(CONVERT_DATE_TYPE_TESTS.values()), - ids=list(CONVERT_DATE_TYPE_TESTS.keys())) + list(_CONVERT_DATE_TYPE_TESTS.values()), + ids=list(_CONVERT_DATE_TYPE_TESTS.keys())) def test_maybe_convert_to_index_date_type(index, date, expected): result = maybe_convert_to_index_date_type(index, date) assert result == expected diff --git a/aospy/utils/times.py b/aospy/utils/times.py index 6a5e9a8..86a4004 100644 --- a/aospy/utils/times.py +++ b/aospy/utils/times.py @@ -202,7 +202,7 @@ def datetime_or_default(date, default): Parameters ---------- - date : `None` or datetime-like object + date : `None` or datetime-like object or str default : The value to return if `date` is `None` Returns @@ -387,9 +387,9 @@ def _assert_has_data_for_time(da, start_date, end_date): ---------- da : DataArray DataArray with a time variable - start_date : netCDF4.netcdftime or np.datetime64 + start_date : datetime-like object or str start date - end_date : netCDF4.netcdftime or np.datetime64 + end_date : datetime-like object or str end date Raises @@ -506,18 +506,36 @@ def ensure_time_as_index(ds): def infer_year(date): - """Given a datetime-like object or string infer the year + """Given a datetime-like object or string infer the year. Parameters ---------- - date : str + date : datetime-like object or str Input date Returns ------- int + + Examples + -------- + >>> infer_year('2000') + 2000 + >>> infer_year('2000-01') + 2000 + >>> infer_year('2000-01-31') + 2000 + >>> infer_year(datetime.datetime(2000, 1, 1)) + 2000 + >>> infer_year(np.datetime64('2000-01-01')) + 2000 + >>> infer_year(DatetimeNoLeap(2000, 1, 1)) + 2000 + >>> """ if isinstance(date, str): + # Look for a string that begins with four numbers; this are the year + # of the partial-datetime string. pattern = '(?P\d{4})' result = re.match(pattern, date) if result: @@ -530,35 +548,23 @@ def infer_year(date): return date.year -def maybe_cast_as_timestamp(date): - """Convert a date to a pd.Timestamp object if possible - - Parameters - ---------- - date : datetime-like object - Input datetime - - Returns - ------- - pd.Timestamp or cftime.datetime - """ - if isinstance(date, str): - return np.datetime64(date).item() - else: - try: - return pd.to_datetime(date) - except (TypeError, OutOfBoundsDatetime): - return date - - def maybe_convert_to_index_date_type(index, date): - """Convert a datetime-like object to the appropriate type + """Convert a datetime-like object to the index's date type. + + Datetime indexing in xarray can be done using either a pandas + DatetimeIndex or a CFTimeIndex. Both support partial-datetime string + indexing regardless of the calendar type of the underlying data; + therefore if a string is passed as a date, we return it unchanged. If a + datetime-like object is provided, it will be converted to the underlying + date type of the index. For a DatetimeIndex that is np.datetime64; for a + CFTimeIndex that is an object of type cftime.datetime specific to the + calendar used. Parameters ---------- index : pd.Index Input time index - date : datetime-like object + date : datetime-like object or str Input datetime Returns diff --git a/docs/examples.rst b/docs/examples.rst index a5e4808..8144043 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -209,16 +209,16 @@ that we saw are directly available as model output: from aospy import Var precip_largescale = Var( - name='precip_largescale', # name used by aospy - alt_names=('condensation_rain',), # its possible name(s) in your data - def_time=True, # whether or not it is defined in time - description='Precipitation generated via grid-scale condensation', + name='precip_largescale', # name used by aospy + alt_names=('condensation_rain',), # its possible name(s) in your data + def_time=True, # whether or not it is defined in time + description='Precipitation generated via grid-scale condensation', ) precip_convective = Var( - name='precip_convective', - alt_names=('convection_rain', 'prec_conv'), - def_time=True, - description='Precipitation generated by convective parameterization', + name='precip_convective', + alt_names=('convection_rain', 'prec_conv'), + def_time=True, + description='Precipitation generated by convective parameterization', ) When it comes time to load data corresponding to either of these from diff --git a/setup.py b/setup.py index 0da5e21..9c1537b 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,8 @@ 'dask >= 0.14', 'distributed >= 1.17.1', 'xarray >= 0.10.4', - 'cloudpickle >= 0.2.1'], + 'cloudpickle >= 0.2.1', + 'cftime >= 1.0.0'], tests_require=['pytest >= 2.7.1', 'pytest-catchlog >= 1.0'], package_data={'aospy': ['test/data/netcdf/*.nc']}, From 6eee1e8bfe734c579ed3357f4da5dd2add575ae6 Mon Sep 17 00:00:00 2001 From: Spencer Clark Date: Tue, 29 May 2018 11:11:51 -0400 Subject: [PATCH 16/17] Clarify comment in infer_year --- aospy/utils/times.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aospy/utils/times.py b/aospy/utils/times.py index 86a4004..fb50eb3 100644 --- a/aospy/utils/times.py +++ b/aospy/utils/times.py @@ -534,8 +534,8 @@ def infer_year(date): >>> """ if isinstance(date, str): - # Look for a string that begins with four numbers; this are the year - # of the partial-datetime string. + # Look for a string that begins with four numbers; the first four + # numbers found are the year. pattern = '(?P\d{4})' result = re.match(pattern, date) if result: From b625714e08d84267bbc219e9f32ae6b12f84deb1 Mon Sep 17 00:00:00 2001 From: Spencer Clark Date: Sat, 2 Jun 2018 09:08:38 -0400 Subject: [PATCH 17/17] Add warning and docs note --- aospy/utils/times.py | 9 ++++++++- docs/examples.rst | 11 +++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/aospy/utils/times.py b/aospy/utils/times.py index fb50eb3..9f8e7bd 100644 --- a/aospy/utils/times.py +++ b/aospy/utils/times.py @@ -1,7 +1,7 @@ """Utility functions for handling times, dates, etc.""" import datetime +import logging import re -import warnings import cftime import numpy as np @@ -398,6 +398,13 @@ def _assert_has_data_for_time(da, start_date, end_date): if the time range is not within the time range of the DataArray """ if isinstance(start_date, str) and isinstance(end_date, str): + logging.warning( + 'When using strings to specify start and end dates, the check ' + 'to determine if data exists for the full extent of the desired ' + 'interval is not implemented. Therefore it is possible that ' + 'you are doing a calculation for a lesser interval than you ' + 'specified. If you would like this check to occur, use explicit ' + 'datetime-like objects for bounds instead.') return if RAW_START_DATE_STR in da.coords: diff --git a/docs/examples.rst b/docs/examples.rst index 8144043..a988850 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -423,6 +423,17 @@ Although we do not show it here, this also prints logging information to the terminal at various steps during each calculation, including the filepaths to the netCDF files written to disk of the results. +.. warning:: + + For date ranges specified using tuples of datetime-like objects, + ``aospy`` will check to make sure that datapoints exist for the full extent + of the time ranges specified. For date ranges specified as tuples of + strings, however, this check is currently not implemented. This is mostly + harmless (i.e. it will not change the results of calculations); however, it + can result in files whose labels do not accurately represent the actual + time bounds of the calculation if you specify string date ranges that span + more than the interval of the input data. + Results =======