Repository: numpy/numpy

Issue:
Parameterize unwrap to allow for different ranges (not just -pi, pi)
The idea would be to make unwrap a more general function applicable to problems like quantization overflow (uint8 254+1 -> 0) and allow for `min_val` and `max_val` parameters (which would default to give the same behavior as currently implemented).
### Reproducing code example:

```python
import numpy as np
np.unwrap([0, 1, 2, 0], min_val=0, max_val=3)
```
should give `array([0., 1., 2., 3.])`


Discussion:
This change would also make it nicer to unwrap degrees instead of radians.
I just had the case of trying to unwrap degrees instead of radians. I changed the `discont` argument to 180, but as the results were weird, I realized that it won't unwrap over 360 degree. I'm looking forward to this pull request being merged! 
The pull request in its current form is not correct, and the author seems to have lost interest. @scimax, if you're interesting in attempting to fix this yourself, please do - I'd recommend you copy the tests, API, and documentation from that PR, but perhaps try and come up with your own implementation since the one there is incorrect.
@eric-wieser I will give it a try. From my first thoughts I would guess it has something to do with the boundaries of interval, and actually with the [question](https://github.com/numpy/numpy/pull/14877#discussion_r351515748) you have already asked. The goal of the line containing the `(ddmod == -pi)` code, is to map the data from the half open interval `[-π,π)` to the half open interval `(-π, π]`, which is also used in [arctan2](https://en.wikipedia.org/wiki/Atan2). Actually, I think the description of the [return value of `np.arctan2`](https://numpy.org/doc/stable/reference/generated/numpy.arctan2.html) is not really correct, as the interval is denoted as `[-pi, pi]`.

I'm confused about the boundary treatment as it compares floating point numbers, which of course is subject to floating points errors.

This example illustrates the use of the line very well (omitting the line `_nx.copyto(ddmod, pi, where=(ddmod == -pi) & (dd > 0))`):
```py
>>> p = np.array([-np.pi, 0, np.pi])
>>> dd = np.diff(p)
>>> dd
array([3.14159265, 3.14159265])
>>> ddmod = np.mod(dd + np.pi, 2*np.pi) - np.pi
>>> ddmod
array([-3.14159265, -3.14159265])
>>> ph_correct = ddmod - dd
>>> ph_correct
array([-6.28318531, -6.28318531])
>>> up = np.array(p, copy=True, dtype='d')
>>> up[1:] = p[1:] + ph_correct.cumsum()
>>> up
array([-3.14159265, -6.28318531, -9.42477796])   # Wrong!
```


Resolving patch:
diff --git a/numpy/lib/function_base.py b/numpy/lib/function_base.py
--- a/numpy/lib/function_base.py
+++ b/numpy/lib/function_base.py
@@ -1482,26 +1482,40 @@ def angle(z, deg=False):
     return a
 
 
-def _unwrap_dispatcher(p, discont=None, axis=None):
+def _unwrap_dispatcher(p, discont=None, axis=None, *, period=None):
     return (p,)
 
 
 @array_function_dispatch(_unwrap_dispatcher)
-def unwrap(p, discont=pi, axis=-1):
-    """
-    Unwrap by changing deltas between values to 2*pi complement.
+def unwrap(p, discont=None, axis=-1, *, period=2*pi):
+    r"""
+    Unwrap by taking the complement of large deltas with respect to the period.
+
+    This unwraps a signal `p` by changing elements which have an absolute
+    difference from their predecessor of more than ``max(discont, period/2)``
+    to their `period`-complementary values.
 
-    Unwrap radian phase `p` by changing absolute jumps greater than
-    `discont` to their 2*pi complement along the given axis.
+    For the default case where `period` is :math:`2\pi` and is `discont` is
+    :math:`\pi`, this unwraps a radian phase `p` such that adjacent differences
+    are never greater than :math:`\pi` by adding :math:`2k\pi` for some
+    integer :math:`k`.
 
     Parameters
     ----------
     p : array_like
         Input array.
     discont : float, optional
-        Maximum discontinuity between values, default is ``pi``.
+        Maximum discontinuity between values, default is ``period/2``. 
+        Values below ``period/2`` are treated as if they were ``period/2``.
+        To have an effect different from the default, `discont` should be
+        larger than ``period/2``.
     axis : int, optional
         Axis along which unwrap will operate, default is the last axis.
+    period: float, optional
+        Size of the range over which the input wraps. By default, it is
+        ``2 pi``.
+        
+        .. versionadded:: 1.21.0
 
     Returns
     -------
@@ -1514,9 +1528,9 @@ def unwrap(p, discont=pi, axis=-1):
 
     Notes
     -----
-    If the discontinuity in `p` is smaller than ``pi``, but larger than
-    `discont`, no unwrapping is done because taking the 2*pi complement
-    would only make the discontinuity larger.
+    If the discontinuity in `p` is smaller than ``period/2``, 
+    but larger than `discont`, no unwrapping is done because taking 
+    the complement would only make the discontinuity larger.
 
     Examples
     --------
@@ -1526,19 +1540,44 @@ def unwrap(p, discont=pi, axis=-1):
     array([ 0.        ,  0.78539816,  1.57079633,  5.49778714,  6.28318531]) # may vary
     >>> np.unwrap(phase)
     array([ 0.        ,  0.78539816,  1.57079633, -0.78539816,  0.        ]) # may vary
-
+    >>> np.unwrap([0, 1, 2, -1, 0], period=4)
+    array([0, 1, 2, 3, 4])
+    >>> np.unwrap([ 1, 2, 3, 4, 5, 6, 1, 2, 3], period=6)
+    array([1, 2, 3, 4, 5, 6, 7, 8, 9])
+    >>> np.unwrap([2, 3, 4, 5, 2, 3, 4, 5], period=4)
+    array([2, 3, 4, 5, 6, 7, 8, 9])
+    >>> phase_deg = np.mod(np.linspace(0 ,720, 19), 360) - 180
+    >>> np.unwrap(phase_deg, period=360)
+    array([-180., -140., -100.,  -60.,  -20.,   20.,   60.,  100.,  140.,
+            180.,  220.,  260.,  300.,  340.,  380.,  420.,  460.,  500.,
+            540.])
     """
     p = asarray(p)
     nd = p.ndim
     dd = diff(p, axis=axis)
+    if discont is None:
+        discont = period/2
     slice1 = [slice(None, None)]*nd     # full slices
     slice1[axis] = slice(1, None)
     slice1 = tuple(slice1)
-    ddmod = mod(dd + pi, 2*pi) - pi
-    _nx.copyto(ddmod, pi, where=(ddmod == -pi) & (dd > 0))
+    dtype = np.result_type(dd, period)
+    if _nx.issubdtype(dtype, _nx.integer):
+        interval_high, rem = divmod(period, 2) 
+        boundary_ambiguous = rem == 0
+    else:
+        interval_high = period / 2
+        boundary_ambiguous = True
+    interval_low = -interval_high
+    ddmod = mod(dd - interval_low, period) + interval_low
+    if boundary_ambiguous:
+        # for `mask = (abs(dd) == period/2)`, the above line made
+        # `ddmod[mask] == -period/2`. correct these such that
+        # `ddmod[mask] == sign(dd[mask])*period/2`.
+        _nx.copyto(ddmod, interval_high,
+                   where=(ddmod == interval_low) & (dd > 0))
     ph_correct = ddmod - dd
     _nx.copyto(ph_correct, 0, where=abs(dd) < discont)
-    up = array(p, copy=True, dtype='d')
+    up = array(p, copy=True, dtype=dtype)
     up[slice1] = p[slice1] + ph_correct.cumsum(axis)
     return up
 


Test patch:
diff --git a/numpy/lib/tests/test_function_base.py b/numpy/lib/tests/test_function_base.py
--- a/numpy/lib/tests/test_function_base.py
+++ b/numpy/lib/tests/test_function_base.py
@@ -1756,6 +1756,24 @@ def test_simple(self):
         assert_array_equal(unwrap([1, 1 + 2 * np.pi]), [1, 1])
         # check that unwrap maintains continuity
         assert_(np.all(diff(unwrap(rand(10) * 100)) < np.pi))
+    
+    def test_period(self):
+        # check that unwrap removes jumps greater that 255
+        assert_array_equal(unwrap([1, 1 + 256], period=255), [1, 2])
+        # check that unwrap maintains continuity
+        assert_(np.all(diff(unwrap(rand(10) * 1000, period=255)) < 255))
+        # check simple case
+        simple_seq = np.array([0, 75, 150, 225, 300])
+        wrap_seq = np.mod(simple_seq, 255)
+        assert_array_equal(unwrap(wrap_seq, period=255), simple_seq)
+        # check custom discont value
+        uneven_seq = np.array([0, 75, 150, 225, 300, 430])
+        wrap_uneven = np.mod(uneven_seq, 250)
+        no_discont = unwrap(wrap_uneven, period=250)
+        assert_array_equal(no_discont, [0, 75, 150, 225, 300, 180])
+        sm_discont = unwrap(wrap_uneven, period=250, discont=140)
+        assert_array_equal(sm_discont, [0, 75, 150, 225, 300, 430])
+        assert sm_discont.dtype == wrap_uneven.dtype
 
 
 class TestFilterwindows: