Repository: numpy/numpy

Issue:
Request: logspace that doesn't require log(a) as argument
linspace(a, b, 10) creates 10 points linearly spaced from a to b.

logspace(a, b, 10) does _not_ create 10 points logarithmically spaced from a to b, as everyone expects it to:
- http://stackoverflow.com/q/31480033/125507
- http://stackoverflow.com/q/31192392/125507
- https://sourceforge.net/p/numpy/mailman/numpy-discussion/thread/Mahogany-0.66.0-0000-00000000-111126.00@CASC64043305.american.edu/

The current behavior can't be changed because of backwards/matlab compatibility, but it would be nice if there were another function that didn't require the extra step of `logspace(log(a), log(b)...`.  

I don't know what it should be called, though.



Discussion:
Can you write up a specific proposal for what the new function's semantics should be, and send it to the mailing list? (In particular it isn't instantly obvious to me what "10 points logarithmically spaced from a to b" means, e.g. does the log base matter?)

I think it would be as simple as:

```
def newlogspace(start, stop, num=50, endpoint=True, base=10.0, dtype=None):
    return numpy.logspace(log(start)/log(base), log(stop)/log(base), num, endpoint, base, dtype)
```

Mostly I just don't know what to call the function, since "logspace" would make sense, but is taken.

And maybe accuracy is slightly better if it has a special case of log10(start) when the base == 10.0, etc.

OK, but it's still the case that if you want this to move forward your next step is to write up a specific proposal and send it to the mailing list :-)

The syntax seems to be taken [from Matlab](http://ch.mathworks.com/help/matlab/ref/logspace.html).

I'm terrible at naming, but your function would build a geometric sequence, so maybe something along the lines of `geomspace`?

Also, getting rid of the `base` parameter seems to make all the sense here.

But we should be having this discussion on the mailing list.

"Logarithmically spaced" is well defined and not base-dependent; `base` only appears in the current `np.logspace()` because it needs to interpret its already-logged arguments. As @jaimefrio points out, it is just a geometric sequence where each successive element is a uniform multiple of its predecessor. This gives you the same result regardless of base, for instance:

```
np.power(base, np.linspace(np.log(start)/np.log(base), np.log(stop)/np.log(base), n))
```

A base-independent implementation would be:

```
def mylogspace(start, stop, num=50, endpoint=True, dtype=None):
    return np.exp(np.linspace(np.log(start), np.log(stop), num=num, endpoint=endpoint, dtype=dtype))
```

@jaimefrio  I just found [spacepy.toolbox.geomspace](http://spacepy.lanl.gov/doc/autosummary/spacepy.toolbox.geomspace.html).  Same name, same syntax. :)


Resolving patch:
diff --git a/numpy/core/function_base.py b/numpy/core/function_base.py
--- a/numpy/core/function_base.py
+++ b/numpy/core/function_base.py
@@ -3,10 +3,12 @@
 import warnings
 import operator
 
-__all__ = ['logspace', 'linspace']
-
 from . import numeric as _nx
-from .numeric import result_type, NaN, shares_memory, MAY_SHARE_BOUNDS, TooHardError
+from .numeric import (result_type, NaN, shares_memory, MAY_SHARE_BOUNDS,
+                      TooHardError)
+
+__all__ = ['logspace', 'linspace', 'geomspace']
+
 
 def _index_deprecate(i, stacklevel=2):
     try:
@@ -73,11 +75,11 @@ def linspace(start, stop, num=50, endpoint=True, retstep=False, dtype=None):
     Examples
     --------
     >>> np.linspace(2.0, 3.0, num=5)
-        array([ 2.  ,  2.25,  2.5 ,  2.75,  3.  ])
+    array([ 2.  ,  2.25,  2.5 ,  2.75,  3.  ])
     >>> np.linspace(2.0, 3.0, num=5, endpoint=False)
-        array([ 2. ,  2.2,  2.4,  2.6,  2.8])
+    array([ 2. ,  2.2,  2.4,  2.6,  2.8])
     >>> np.linspace(2.0, 3.0, num=5, retstep=True)
-        (array([ 2.  ,  2.25,  2.5 ,  2.75,  3.  ]), 0.25)
+    (array([ 2.  ,  2.25,  2.5 ,  2.75,  3.  ]), 0.25)
 
     Graphical illustration:
 
@@ -102,8 +104,8 @@ def linspace(start, stop, num=50, endpoint=True, retstep=False, dtype=None):
     div = (num - 1) if endpoint else num
 
     # Convert float/complex array scalars to float, gh-3504
-    start = start * 1.
-    stop = stop * 1.
+    start = start * 1.0
+    stop = stop * 1.0
 
     dt = result_type(start, stop, float(num))
     if dtype is None:
@@ -156,7 +158,7 @@ def logspace(start, stop, num=50, endpoint=True, base=10.0, dtype=None):
         ``base ** stop`` is the final value of the sequence, unless `endpoint`
         is False.  In that case, ``num + 1`` values are spaced over the
         interval in log-space, of which all but the last (a sequence of
-        length ``num``) are returned.
+        length `num`) are returned.
     num : integer, optional
         Number of samples to generate.  Default is 50.
     endpoint : boolean, optional
@@ -195,11 +197,11 @@ def logspace(start, stop, num=50, endpoint=True, base=10.0, dtype=None):
     Examples
     --------
     >>> np.logspace(2.0, 3.0, num=4)
-        array([  100.        ,   215.443469  ,   464.15888336,  1000.        ])
+    array([  100.        ,   215.443469  ,   464.15888336,  1000.        ])
     >>> np.logspace(2.0, 3.0, num=4, endpoint=False)
-        array([ 100.        ,  177.827941  ,  316.22776602,  562.34132519])
+    array([ 100.        ,  177.827941  ,  316.22776602,  562.34132519])
     >>> np.logspace(2.0, 3.0, num=4, base=2.0)
-        array([ 4.        ,  5.0396842 ,  6.34960421,  8.        ])
+    array([ 4.        ,  5.0396842 ,  6.34960421,  8.        ])
 
     Graphical illustration:
 
@@ -221,3 +223,127 @@ def logspace(start, stop, num=50, endpoint=True, base=10.0, dtype=None):
     if dtype is None:
         return _nx.power(base, y)
     return _nx.power(base, y).astype(dtype)
+
+
+def geomspace(start, stop, num=50, endpoint=True, dtype=None):
+    """
+    Return numbers spaced evenly on a log scale (a geometric progression).
+
+    This is similar to `logspace`, but with endpoints specified directly.
+    Each output sample is a constant multiple of the previous.
+
+    Parameters
+    ----------
+    start : scalar
+        The starting value of the sequence.
+    stop : scalar
+        The final value of the sequence, unless `endpoint` is False.
+        In that case, ``num + 1`` values are spaced over the
+        interval in log-space, of which all but the last (a sequence of
+        length `num`) are returned.
+    num : integer, optional
+        Number of samples to generate.  Default is 50.
+    endpoint : boolean, optional
+        If true, `stop` is the last sample. Otherwise, it is not included.
+        Default is True.
+    dtype : dtype
+        The type of the output array.  If `dtype` is not given, infer the data
+        type from the other input arguments.
+
+    Returns
+    -------
+    samples : ndarray
+        `num` samples, equally spaced on a log scale.
+
+    See Also
+    --------
+    logspace : Similar to geomspace, but with endpoints specified using log
+               and base.
+    linspace : Similar to geomspace, but with arithmetic instead of geometric
+               progression.
+    arange : Similar to linspace, with the step size specified instead of the
+             number of samples.
+
+    Notes
+    -----
+    If the inputs or dtype are complex, the output will follow a logarithmic
+    spiral in the complex plane.  (There are an infinite number of spirals
+    passing through two points; the output will follow the shortest such path.)
+
+    Examples
+    --------
+    >>> np.geomspace(1, 1000, num=4)
+    array([    1.,    10.,   100.,  1000.])
+    >>> np.geomspace(1, 1000, num=3, endpoint=False)
+    array([   1.,   10.,  100.])
+    >>> np.geomspace(1, 1000, num=4, endpoint=False)
+    array([   1.        ,    5.62341325,   31.6227766 ,  177.827941  ])
+    >>> np.geomspace(1, 256, num=9)
+    array([   1.,    2.,    4.,    8.,   16.,   32.,   64.,  128.,  256.])
+
+    Note that the above may not produce exact integers:
+
+    >>> np.geomspace(1, 256, num=9, dtype=int)
+    array([  1,   2,   4,   7,  16,  32,  63, 127, 256])
+    >>> np.around(np.geomspace(1, 256, num=9)).astype(int)
+    array([  1,   2,   4,   8,  16,  32,  64, 128, 256])
+
+    Negative, decreasing, and complex inputs are allowed:
+
+    >>> geomspace(1000, 1, num=4)
+    array([ 1000.,   100.,    10.,     1.])
+    >>> geomspace(-1000, -1, num=4)
+    array([-1000.,  -100.,   -10.,    -1.])
+    >>> geomspace(1j, 1000j, num=4)  # Straight line
+    array([ 0.   +1.j,  0.  +10.j,  0. +100.j,  0.+1000.j])
+    >>> geomspace(-1+0j, 1+0j, num=5)  # Circle
+    array([-1.00000000+0.j        , -0.70710678+0.70710678j,
+            0.00000000+1.j        ,  0.70710678+0.70710678j,
+            1.00000000+0.j        ])
+
+    Graphical illustration of ``endpoint`` parameter:
+
+    >>> import matplotlib.pyplot as plt
+    >>> N = 10
+    >>> y = np.zeros(N)
+    >>> plt.semilogx(np.geomspace(1, 1000, N, endpoint=True), y + 1, 'o')
+    >>> plt.semilogx(np.geomspace(1, 1000, N, endpoint=False), y + 2, 'o')
+    >>> plt.axis([0.5, 2000, 0, 3])
+    >>> plt.grid(True, color='0.7', linestyle='-', which='both', axis='both')
+    >>> plt.show()
+
+    """
+    if start == 0 or stop == 0:
+        raise ValueError('Geometric sequence cannot include zero')
+
+    dt = result_type(start, stop, float(num))
+    if dtype is None:
+        dtype = dt
+    else:
+        # complex to dtype('complex128'), for instance
+        dtype = _nx.dtype(dtype)
+
+    # Avoid negligible real or imaginary parts in output by rotating to
+    # positive real, calculating, then undoing rotation
+    out_sign = 1
+    if start.real == stop.real == 0:
+        start, stop = start.imag, stop.imag
+        out_sign = 1j * out_sign
+    if _nx.sign(start) == _nx.sign(stop) == -1:
+        start, stop = -start, -stop
+        out_sign = -out_sign
+
+    # Promote both arguments to the same dtype in case, for instance, one is
+    # complex and another is negative and log would produce NaN otherwise
+    start = start + (stop - stop)
+    stop = stop + (start - start)
+    if _nx.issubdtype(dtype, complex):
+        start = start + 0j
+        stop = stop + 0j
+
+    log_start = _nx.log10(start)
+    log_stop = _nx.log10(stop)
+    result = out_sign * logspace(log_start, log_stop, num=num,
+                                 endpoint=endpoint, base=10.0, dtype=dtype)
+
+    return result.astype(dtype)


Test patch:
diff --git a/numpy/core/tests/test_function_base.py b/numpy/core/tests/test_function_base.py
--- a/numpy/core/tests/test_function_base.py
+++ b/numpy/core/tests/test_function_base.py
@@ -1,13 +1,45 @@
 from __future__ import division, absolute_import, print_function
 
-from numpy import (logspace, linspace, dtype, array, finfo, typecodes, arange,
-                   isnan, ndarray)
+from numpy import (logspace, linspace, geomspace, dtype, array, finfo,
+                   typecodes, arange, isnan, ndarray, sqrt)
 from numpy.testing import (
     TestCase, run_module_suite, assert_, assert_equal, assert_raises,
-    assert_array_equal
+    assert_array_equal, assert_allclose
 )
 
 
+class PhysicalQuantity(float):
+    def __new__(cls, value):
+        return float.__new__(cls, value)
+
+    def __add__(self, x):
+        assert_(isinstance(x, PhysicalQuantity))
+        return PhysicalQuantity(float(x) + float(self))
+    __radd__ = __add__
+
+    def __sub__(self, x):
+        assert_(isinstance(x, PhysicalQuantity))
+        return PhysicalQuantity(float(self) - float(x))
+
+    def __rsub__(self, x):
+        assert_(isinstance(x, PhysicalQuantity))
+        return PhysicalQuantity(float(x) - float(self))
+
+    def __mul__(self, x):
+        return PhysicalQuantity(float(x) * float(self))
+    __rmul__ = __mul__
+
+    def __div__(self, x):
+        return PhysicalQuantity(float(self) / float(x))
+
+    def __rdiv__(self, x):
+        return PhysicalQuantity(float(x) / float(self))
+
+
+class PhysicalQuantity2(ndarray):
+    __array_priority__ = 10
+
+
 class TestLogspace(TestCase):
 
     def test_basic(self):
@@ -28,6 +60,136 @@ def test_dtype(self):
         y = logspace(0, 6, dtype='int32')
         assert_equal(y.dtype, dtype('int32'))
 
+    def test_physical_quantities(self):
+        a = PhysicalQuantity(1.0)
+        b = PhysicalQuantity(5.0)
+        assert_equal(logspace(a, b), logspace(1.0, 5.0))
+
+    def test_subclass(self):
+        a = array(1).view(PhysicalQuantity2)
+        b = array(7).view(PhysicalQuantity2)
+        ls = logspace(a, b)
+        assert type(ls) is PhysicalQuantity2
+        assert_equal(ls, logspace(1.0, 7.0))
+        ls = logspace(a, b, 1)
+        assert type(ls) is PhysicalQuantity2
+        assert_equal(ls, logspace(1.0, 7.0, 1))
+
+
+class TestGeomspace(TestCase):
+
+    def test_basic(self):
+        y = geomspace(1, 1e6)
+        assert_(len(y) == 50)
+        y = geomspace(1, 1e6, num=100)
+        assert_(y[-1] == 10 ** 6)
+        y = geomspace(1, 1e6, endpoint=False)
+        assert_(y[-1] < 10 ** 6)
+        y = geomspace(1, 1e6, num=7)
+        assert_array_equal(y, [1, 10, 100, 1e3, 1e4, 1e5, 1e6])
+
+        y = geomspace(8, 2, num=3)
+        assert_allclose(y, [8, 4, 2])
+        assert_array_equal(y.imag, 0)
+
+        y = geomspace(-1, -100, num=3)
+        assert_array_equal(y, [-1, -10, -100])
+        assert_array_equal(y.imag, 0)
+
+        y = geomspace(-100, -1, num=3)
+        assert_array_equal(y, [-100, -10, -1])
+        assert_array_equal(y.imag, 0)
+
+    def test_complex(self):
+        # Purely imaginary
+        y = geomspace(1j, 16j, num=5)
+        assert_allclose(y, [1j, 2j, 4j, 8j, 16j])
+        assert_array_equal(y.real, 0)
+
+        y = geomspace(-4j, -324j, num=5)
+        assert_allclose(y, [-4j, -12j, -36j, -108j, -324j])
+        assert_array_equal(y.real, 0)
+
+        y = geomspace(1+1j, 1000+1000j, num=4)
+        assert_allclose(y, [1+1j, 10+10j, 100+100j, 1000+1000j])
+
+        y = geomspace(-1+1j, -1000+1000j, num=4)
+        assert_allclose(y, [-1+1j, -10+10j, -100+100j, -1000+1000j])
+
+        # Logarithmic spirals
+        y = geomspace(-1, 1, num=3, dtype=complex)
+        assert_allclose(y, [-1, 1j, +1])
+
+        y = geomspace(0+3j, -3+0j, 3)
+        assert_allclose(y, [0+3j, -3/sqrt(2)+3j/sqrt(2), -3+0j])
+        y = geomspace(0+3j, 3+0j, 3)
+        assert_allclose(y, [0+3j, 3/sqrt(2)+3j/sqrt(2), 3+0j])
+        y = geomspace(-3+0j, 0-3j, 3)
+        assert_allclose(y, [-3+0j, -3/sqrt(2)-3j/sqrt(2), 0-3j])
+        y = geomspace(0+3j, -3+0j, 3)
+        assert_allclose(y, [0+3j, -3/sqrt(2)+3j/sqrt(2), -3+0j])
+        y = geomspace(-2-3j, 5+7j, 7)
+        assert_allclose(y, [-2-3j, -0.29058977-4.15771027j,
+                            2.08885354-4.34146838j, 4.58345529-3.16355218j,
+                            6.41401745-0.55233457j, 6.75707386+3.11795092j,
+                            5+7j])
+
+        # Type promotion should prevent the -5 from becoming a NaN
+        y = geomspace(3j, -5, 2)
+        assert_allclose(y, [3j, -5])
+        y = geomspace(-5, 3j, 2)
+        assert_allclose(y, [-5, 3j])
+
+    def test_dtype(self):
+        y = geomspace(1, 1e6, dtype='float32')
+        assert_equal(y.dtype, dtype('float32'))
+        y = geomspace(1, 1e6, dtype='float64')
+        assert_equal(y.dtype, dtype('float64'))
+        y = geomspace(1, 1e6, dtype='int32')
+        assert_equal(y.dtype, dtype('int32'))
+
+        # Native types
+        y = geomspace(1, 1e6, dtype=float)
+        assert_equal(y.dtype, dtype('float_'))
+        y = geomspace(1, 1e6, dtype=complex)
+        assert_equal(y.dtype, dtype('complex'))
+
+    def test_array_scalar(self):
+        lim1 = array([120, 100], dtype="int8")
+        lim2 = array([-120, -100], dtype="int8")
+        lim3 = array([1200, 1000], dtype="uint16")
+        t1 = geomspace(lim1[0], lim1[1], 5)
+        t2 = geomspace(lim2[0], lim2[1], 5)
+        t3 = geomspace(lim3[0], lim3[1], 5)
+        t4 = geomspace(120.0, 100.0, 5)
+        t5 = geomspace(-120.0, -100.0, 5)
+        t6 = geomspace(1200.0, 1000.0, 5)
+
+        # t3 uses float32, t6 uses float64
+        assert_allclose(t1, t4, rtol=1e-2)
+        assert_allclose(t2, t5, rtol=1e-2)
+        assert_allclose(t3, t6, rtol=1e-5)
+
+    def test_physical_quantities(self):
+        a = PhysicalQuantity(1.0)
+        b = PhysicalQuantity(5.0)
+        assert_equal(geomspace(a, b), geomspace(1.0, 5.0))
+
+    def test_subclass(self):
+        a = array(1).view(PhysicalQuantity2)
+        b = array(7).view(PhysicalQuantity2)
+        gs = geomspace(a, b)
+        assert type(gs) is PhysicalQuantity2
+        assert_equal(gs, geomspace(1.0, 7.0))
+        gs = geomspace(a, b, 1)
+        assert type(gs) is PhysicalQuantity2
+        assert_equal(gs, geomspace(1.0, 7.0, 1))
+
+    def test_bounds(self):
+        assert_raises(ValueError, geomspace, 0, 10)
+        assert_raises(ValueError, geomspace, 10, 0)
+        assert_raises(ValueError, geomspace, 0, 0)
+
 
 class TestLinspace(TestCase):
 
@@ -77,48 +239,18 @@ def test_array_scalar(self):
 
     def test_complex(self):
         lim1 = linspace(1 + 2j, 3 + 4j, 5)
-        t1 = array([ 1.0+2.j,  1.5+2.5j,  2.0+3.j,  2.5+3.5j,  3.0+4.j])
+        t1 = array([1.0+2.j, 1.5+2.5j,  2.0+3j, 2.5+3.5j, 3.0+4j])
         lim2 = linspace(1j, 10, 5)
-        t2 = array([  0.0+1.j,   2.5+0.75j,   5.0+0.5j,   7.5+0.25j,  10.0+0.j])
+        t2 = array([0.0+1.j, 2.5+0.75j, 5.0+0.5j, 7.5+0.25j, 10.0+0j])
         assert_equal(lim1, t1)
         assert_equal(lim2, t2)
 
     def test_physical_quantities(self):
-        class PhysicalQuantity(float):
-            def __new__(cls, value):
-                return float.__new__(cls, value)
-
-            def __add__(self, x):
-                assert_(isinstance(x, PhysicalQuantity))
-                return PhysicalQuantity(float(x) + float(self))
-            __radd__ = __add__
-
-            def __sub__(self, x):
-                assert_(isinstance(x, PhysicalQuantity))
-                return PhysicalQuantity(float(self) - float(x))
-
-            def __rsub__(self, x):
-                assert_(isinstance(x, PhysicalQuantity))
-                return PhysicalQuantity(float(x) - float(self))
-
-            def __mul__(self, x):
-                return PhysicalQuantity(float(x) * float(self))
-            __rmul__ = __mul__
-
-            def __div__(self, x):
-                return PhysicalQuantity(float(self) / float(x))
-
-            def __rdiv__(self, x):
-                return PhysicalQuantity(float(x) / float(self))
-
         a = PhysicalQuantity(0.0)
         b = PhysicalQuantity(1.0)
         assert_equal(linspace(a, b), linspace(0.0, 1.0))
 
     def test_subclass(self):
-        class PhysicalQuantity2(ndarray):
-            __array_priority__ = 10
-
         a = array(0).view(PhysicalQuantity2)
         b = array(1).view(PhysicalQuantity2)
         ls = linspace(a, b)