[Git][debian-gis-team/cftime][master] 4 commits: New upstream version 1.6.0+ds

Bas Couwenberg (@sebastic) gitlab at salsa.debian.org
Fri Mar 4 06:10:14 GMT 2022



Bas Couwenberg pushed to branch master at Debian GIS Project / cftime


Commits:
72b4e4f0 by Bas Couwenberg at 2022-03-04T06:51:44+01:00
New upstream version 1.6.0+ds
- - - - -
5eb68cb4 by Bas Couwenberg at 2022-03-04T06:51:45+01:00
Update upstream source from tag 'upstream/1.6.0+ds'

Update to upstream version '1.6.0+ds'
with Debian dir f556abe2c60a35f3fe78f2e869efe30478525b14
- - - - -
b90b4f64 by Bas Couwenberg at 2022-03-04T06:53:42+01:00
New upstream release.

- - - - -
1d4e1c6b by Bas Couwenberg at 2022-03-04T06:54:11+01:00
Set distribution to unstable.

- - - - -


7 changed files:

- + .github/workflows/build.yml
- .github/workflows/miniconda.yml
- Changelog
- README.md
- debian/changelog
- src/cftime/_cftime.pyx
- test/test_cftime.py


Changes:

=====================================
.github/workflows/build.yml
=====================================
@@ -0,0 +1,34 @@
+name: Build and test with development python
+on: [push, pull_request]
+jobs:
+  build-linux:
+    name: Python (${{ matrix.python-version }})
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        python-version: ["3.11-dev"]
+    steps:
+
+    - uses: actions/checkout at v2
+
+    - name: Set up Python ${{ matrix.python-version }}
+      uses: actions/setup-python at v2
+      with:
+        python-version: ${{ matrix.python-version }}
+
+    - name: Update Pip
+      run: |
+        python -m pip install --upgrade pip
+
+    - name: Install cftime dependencies via pip
+      run: |
+        python -m pip install -r requirements.txt
+        python -m pip install -r requirements-dev.txt
+
+    - name: Install cftime
+      run: |
+        python setup.py install
+
+    - name: Test cftime
+      run: |
+        py.test -vv test


=====================================
.github/workflows/miniconda.yml
=====================================
@@ -10,7 +10,7 @@ jobs:
     runs-on: ${{ matrix.os }}
     strategy:
       matrix:
-        python-version: [ "3.7", "3.8", "3.9", "3.10"]
+        python-version: [ "3.7", "3.8", "3.9", "3.10" ]
         os: [windows-latest, ubuntu-latest, macos-latest]
         platform: [x64, x32]
 #  debug on a single os/platform/python version


=====================================
Changelog
=====================================
@@ -1,3 +1,11 @@
+version 1.6.0 (release tag v1.6.0rel)
+=====================================
+ * fix for masked array inputs (issue #267).
+ * improved performance of the num2date algorithm, in some cases providing
+   an over 100x speedup (issue #269, PR#270).
+ * fix for date2index for select != 'exact' when select='exact' works (issue
+   #272, PR#273)
+
 version 1.5.2 (release tag v1.5.2rel)
 =====================================
  * silently change calendar='gregorian' to 'standard' internally, 


=====================================
README.md
=====================================
@@ -12,6 +12,8 @@ Time-handling functionality from netcdf4-python
 ## News
 For details on the latest updates, see the [Changelog](https://github.com/Unidata/cftime/blob/master/Changelog).
 
+3/4/2022:  Version 1.6.0 released.  Big speed-ups for num2date, date2index bugfix for select != 'exact' when select='exact' works, fix for date2num with masked array inputs.
+
 1/22/2022: Version 1.5.2 released (wheels for Apple M1 available on pypi for python 3.8,3.9 and 3.10). is_leap_year
 function added (issue #259).
 


=====================================
debian/changelog
=====================================
@@ -1,3 +1,9 @@
+cftime (1.6.0+ds-1) unstable; urgency=medium
+
+  * New upstream release.
+
+ -- Bas Couwenberg <sebastic at debian.org>  Fri, 04 Mar 2022 06:53:59 +0100
+
 cftime (1.5.2+ds-1) unstable; urgency=medium
 
   * New upstream release.


=====================================
src/cftime/_cftime.pyx
=====================================
@@ -38,7 +38,7 @@ cdef int[12] _dayspermonth_leap = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 3
 cdef int[13] _cumdayspermonth = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365]
 cdef int[13] _cumdayspermonth_leap = [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366]
 
-__version__ = '1.5.2'
+__version__ = '1.6.0'
 
 # Adapted from http://delete.me.uk/2005/03/iso8601.html
 # Note: This regex ensures that all ISO8601 timezone formats are accepted - but, due to legacy support for other timestrings, not all incorrect formats can be rejected.
@@ -254,8 +254,12 @@ def date2num(dates,units,calendar=None,has_year_zero=None):
         use_python_datetime = False
         # convert basedate to specified calendar
         basedate =  to_calendar_specific_datetime(basedate, calendar, False, has_year_zero=has_year_zero)
-    times = []; n = 0
-    for date in dates.flat:
+    times = []
+    for n, date in enumerate(dates.flat):
+        if ismasked and mask.flat[n]:
+            times.append(None)
+            continue
+
         # use python datetime if possible.
         if use_python_datetime:
             # remove time zone offset
@@ -263,20 +267,18 @@ def date2num(dates,units,calendar=None,has_year_zero=None):
                 date = date.replace(tzinfo=None) - date.utcoffset()
         else: # convert date to same calendar specific cftime.datetime instance
             date = to_calendar_specific_datetime(date, calendar, False, has_year_zero=has_year_zero)
-        if ismasked and mask.flat[n]:
-            times.append(None)
+
+        td = date - basedate
+        if td % unit_timedelta == timedelta(0):
+            # Explicitly cast result to np.int64 for Windows compatibility
+            quotient = np.int64(td // unit_timedelta)
+            times.append(quotient)
         else:
-            td = date - basedate
-            if td % unit_timedelta == timedelta(0):
-                # Explicitly cast result to np.int64 for Windows compatibility
-                quotient = np.int64(td // unit_timedelta)
-                times.append(quotient)
-            else:
-                times.append(td / unit_timedelta)
-        n += 1
+            times.append(td / unit_timedelta)
+                
     if ismasked: # convert to masked array if input was masked array
-        times = np.array(times)
-        times = np.ma.masked_where(times==None,times)
+        times = np.array(times, dtype=float)  # None -> nan
+        times = np.ma.masked_invalid(times)
         if isscalar:
             return times[0]
         else:
@@ -424,6 +426,52 @@ def scale_times(num, factor):
         else:
             return num * factor
 
+
+def decode_date_from_scalar(time_in_microseconds, basedate):
+    """Decode a date from a scalar input."""
+    delta = time_in_microseconds.astype("timedelta64[us]").astype(timedelta)
+    try:
+        return basedate + delta
+    except OverflowError:
+        raise ValueError("OverflowError in datetime, possibly because year < datetime.MINYEAR")
+
+
+def decode_dates_from_array(times_in_microseconds, basedate):
+    """Decode values encoded by an integer array in units of microseconds to dates.
+    
+    This is an optimized algorithm that operates by flattening and sorting the input
+    array of integers, decoding the first date using the original base date, and then
+    incrementally adding timedeltas to decode the rest of the dates in the array.  This
+    is an optimal approach, because it minimizes the length of the timedeltas used in
+    each addition operation required to decode the times (timedelta addition is the rate
+    limiting step in the process).  The original order of the elements and shape of the
+    array are restored at the end.  The sorting and unsorting steps add only a small
+    overhead.  See discussion and timing results in GitHub issue 269.
+    """
+    original_shape = times_in_microseconds.shape
+    times_in_microseconds = times_in_microseconds.ravel()
+
+    sort_indices = np.argsort(times_in_microseconds)
+    unsort_indices = np.argsort(sort_indices)
+    times_in_microseconds = times_in_microseconds[sort_indices]
+
+    # We first cast to the np.timedelta64[us] dtype out of convenience, but ultimately
+    # cast to datetime.timedelta objects for operations with cftime objects (we cannot
+    # cast from integers to datetime.timedelta objects directly).
+    deltas = times_in_microseconds.astype("timedelta64[us]")
+    differential_deltas = np.diff(deltas).astype(timedelta)
+
+    dates = np.empty(times_in_microseconds.shape, dtype="O")
+    try:
+        dates[0] = basedate + deltas[0].astype(timedelta)
+        for i in range(len(differential_deltas)):
+            dates[i + 1] = dates[i] + differential_deltas[i]
+    except OverflowError:
+        raise ValueError("OverflowError in datetime, possibly because year < datetime.MINYEAR")
+
+    return dates[unsort_indices].reshape(original_shape)
+
+
 @cython.embedsignature(True)
 def num2date(
     times,
@@ -535,14 +583,18 @@ def num2date(
     scaled_times = scale_times(times, factor)
     scaled_times = cast_to_int(scaled_times,units=unit)
 
-    # Through np.timedelta64, convert integers scaled to have units of
-    # microseconds to datetime.timedelta objects, the timedelta type compatible
-    # with all cftime.datetime objects.
-    deltas = scaled_times.astype("timedelta64[us]").astype(timedelta)
-    try:
-        return basedate + deltas
-    except OverflowError:
-        raise ValueError("OverflowError in datetime, possibly because year < datetime.MINYEAR")
+    if scaled_times.ndim == 0 or scaled_times.size == 0:
+        return decode_date_from_scalar(scaled_times, basedate)
+    else:
+        if isinstance(scaled_times, np.ma.MaskedArray):
+            # The algorithm requires data be present for all values. To handle this, we fill 
+            # masked values with 0 temporarily and then restore the mask at the end.
+            original_mask = np.ma.getmask(scaled_times)
+            scaled_times = scaled_times.filled(0)
+            dates = decode_dates_from_array(scaled_times, basedate)
+            return np.ma.MaskedArray(dates, mask=original_mask)
+        else:
+            return decode_dates_from_array(scaled_times, basedate)
 
 
 @cython.embedsignature(True)
@@ -839,6 +891,20 @@ def time2index(times, nctime, calendar=None, select='exact'):
     if calendar == None:
         calendar = getattr(nctime, 'calendar', 'standard')
 
+    if select != 'exact':
+        # if select works, then 'nearest' == 'exact', 'before' == 'exact'-1 and
+        # 'after' == 'exact'+1
+        try:
+            index = time2index(times, nctime, calendar=calendar, select='exact')
+            if select == 'nearest':
+                return index
+            elif select == 'before':
+                return index-1
+            else:
+                return index+1
+        except ValueError:
+            pass
+
     num = np.atleast_1d(times)
     N = len(nctime)
 


=====================================
test/test_cftime.py
=====================================
@@ -961,6 +961,10 @@ class TestDate2index(unittest.TestCase):
         self.standardtime = self.TestTime(datetime(1950, 1, 1), 366, 24,
                                           'hours since 1900-01-01', 'standard')
 
+        self.issue272time = self.TestTime(datetime(1950, 1, 1), 5, 24,
+                                          'hours since 1900-01-01', 'standard')
+        self.issue272time._data=np.array([1053144, 1053150, 1053156, 1053157,
+            1053162],np.int32)
         self.time_vars = {}
         self.time_vars['time'] = CFTimeVariable(
             values=self.standardtime,
@@ -1117,6 +1121,18 @@ class TestDate2index(unittest.TestCase):
                            select='nearest')
         assert(index == 11)
 
+    def test_issue272(self):
+        timeArray = self.issue272time
+        date = datetime(2020, 2, 22, 13)
+        assert(date2index(date, timeArray, calendar="gregorian",
+            select="exact")==3)
+        assert(date2index(date, timeArray, calendar="gregorian",
+            select="before")==2)
+        assert(date2index(date, timeArray, calendar="gregorian",
+            select="after")==4)
+        assert(date2index(date, timeArray, calendar="gregorian",
+            select="nearest")==3)
+
 
 class issue584TestCase(unittest.TestCase):
     """Regression tests for issue #584."""
@@ -2037,6 +2053,53 @@ def test_date2num_num2date_roundtrip(encoding_units, freq, calendar):
         meets_tolerance = np.abs(decoded - times) <= tolerance
         assert np.all(meets_tolerance)
 
+def test_date2num_missing_data():
+    # Masked array
+    a = [
+        cftime.DatetimeGregorian(2000, 12, 1),
+        cftime.DatetimeGregorian(2000, 12, 2),
+        cftime.DatetimeGregorian(2000, 12, 3),
+        cftime.DatetimeGregorian(2000, 12, 4),
+    ]
+    mask = [True, False, True, False]
+    array = np.ma.array(a, mask=mask)
+    out = date2num(array, units="days since 2000-12-01", calendar="standard")
+    assert ((out == np.ma.array([-99, 1, -99, 3] , mask=mask)).all())
+    assert ((out.mask == mask).all())
+
+    # Scalar masked array
+    a = cftime.DatetimeGregorian(2000, 12, 1)
+    mask = True
+    array = np.ma.array(a, mask=mask)
+    out = date2num(array, units="days since 2000-12-01", calendar="standard")
+    assert out is np.ma.masked
+
+
+def test_num2date_preserves_shape():
+    # The optimized num2date algorithm operates on a flattened array.  This
+    # check ensures that the original shape of the times is restored in the 
+    # result.
+    a = np.array([[0, 1, 2], [3, 4, 5]])
+    result = num2date(a, units="days since 2000-01-01", calendar="standard")
+    expected = np.array([cftime.DatetimeGregorian(2000, 1, i) for i in range(1, 7)]).reshape((2, 3))
+    np.testing.assert_equal(result, expected)
+
+
+def test_num2date_preserves_order():
+    # The optimized num2date algorithm sorts the encoded times before decoding them.
+    # This check ensures that the order of the times is restored in the result.
+    a = np.array([1, 0])
+    result = num2date(a, units="days since 2000-01-01", calendar="standard")
+    expected = np.array([cftime.DatetimeGregorian(2000, 1, i) for i in [2, 1]])
+    np.testing.assert_equal(result, expected)
+
+
+def test_num2date_empty_array():
+    a = np.array([[]])
+    result = num2date(a, units="days since 2000-01-01", calendar="standard")
+    expected = np.array([[]], dtype="O")
+    np.testing.assert_equal(result, expected)
+
 
 if __name__ == '__main__':
     unittest.main()



View it on GitLab: https://salsa.debian.org/debian-gis-team/cftime/-/compare/3655dd74d4278aac61d48e8ae31d8db016ffab50...1d4e1c6b6136ac45b7e9bbd0a5908e69f60d4724

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/cftime/-/compare/3655dd74d4278aac61d48e8ae31d8db016ffab50...1d4e1c6b6136ac45b7e9bbd0a5908e69f60d4724
You're receiving this email because of your account on salsa.debian.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20220304/295e4d92/attachment-0001.htm>


More information about the Pkg-grass-devel mailing list