diff options
-rw-r--r-- | README.md | 13 | ||||
-rw-r--r-- | doc/source/docs/howto_build_docs.rst | 15 | ||||
-rw-r--r-- | doc/source/reference/arrays.classes.rst | 126 | ||||
-rw-r--r-- | doc/source/reference/c-api.ufunc.rst | 22 | ||||
-rw-r--r-- | doc/source/reference/random/index.rst | 12 | ||||
-rw-r--r-- | doc/source/user/basics.dispatch.rst | 8 | ||||
-rw-r--r-- | doc/source/user/basics.rst | 1 | ||||
-rw-r--r-- | numpy/core/function_base.py | 40 | ||||
-rw-r--r-- | numpy/core/include/numpy/ufuncobject.h | 12 | ||||
-rw-r--r-- | numpy/core/src/multiarray/arraytypes.c.src | 4 | ||||
-rw-r--r-- | numpy/core/src/multiarray/ctors.c | 7 | ||||
-rw-r--r-- | numpy/core/tests/test_multiarray.py | 5 | ||||
-rw-r--r-- | numpy/distutils/system_info.py | 2 | ||||
-rw-r--r-- | numpy/doc/dispatch.py | 271 | ||||
-rw-r--r-- | numpy/fft/pocketfft.py | 42 | ||||
-rw-r--r-- | numpy/polynomial/polyutils.py | 1 | ||||
-rw-r--r-- | numpy/polynomial/tests/test_classes.py | 13 | ||||
-rw-r--r-- | numpy/polynomial/tests/test_polynomial.py | 4 | ||||
-rw-r--r-- | tools/npy_tempita/__init__.py | 56 |
19 files changed, 562 insertions, 92 deletions
@@ -35,4 +35,17 @@ Tests can then be run after installation with: python -c 'import numpy; numpy.test()' + +Call for Contributions +---------------------- + +NumPy appreciates help from a wide range of different backgrounds. +Work such as high level documentation or website improvements are valuable +and we would like to grow our team with people filling these roles. +Small improvements or fixes are always appreciated and issues labeled as easy +may be a good starting point. +If you are considering larger contributions outside the traditional coding work, +please contact us through the mailing list. + + [](https://numfocus.org) diff --git a/doc/source/docs/howto_build_docs.rst b/doc/source/docs/howto_build_docs.rst index 98d1b88ba..4bb7628c1 100644 --- a/doc/source/docs/howto_build_docs.rst +++ b/doc/source/docs/howto_build_docs.rst @@ -30,11 +30,9 @@ In addition, building the documentation requires the Sphinx extension `plot_directive`, which is shipped with Matplotlib_. This Sphinx extension can be installed by installing Matplotlib. You will also need python3.6. -Since large parts of the main documentation are stored in -docstrings, you will need to first build NumPy, and install it so -that the correct version is imported by - - >>> import numpy +Since large parts of the main documentation are obtained from numpy via +``import numpy`` and examining the docstrings, you will need to first build +NumPy, and install it so that the correct version is imported. Note that you can eg. install NumPy to a temporary location and set the PYTHONPATH environment variable appropriately. @@ -46,8 +44,11 @@ generate the docs, so write:: make html in the ``doc/`` directory. If all goes well, this will generate a -``build/html`` subdirectory containing the built documentation. Note -that building the documentation on Windows is currently not actively +``build/html`` subdirectory containing the built documentation. If you get +a message about ``installed numpy != current repo git version``, you must +either override the check by setting ``GITVER`` or re-install NumPy. + +Note that building the documentation on Windows is currently not actively supported, though it should be possible. (See Sphinx_ documentation for more information.) diff --git a/doc/source/reference/arrays.classes.rst b/doc/source/reference/arrays.classes.rst index 3b13530c7..a91215476 100644 --- a/doc/source/reference/arrays.classes.rst +++ b/doc/source/reference/arrays.classes.rst @@ -6,8 +6,15 @@ Standard array subclasses .. currentmodule:: numpy -The :class:`ndarray` in NumPy is a "new-style" Python -built-in-type. Therefore, it can be inherited from (in Python or in C) +.. note:: + + Subclassing a ``numpy.ndarray`` is possible but if your goal is to create + an array with *modified* behavior, as do dask arrays for distributed + computation and cupy arrays for GPU-based computation, subclassing is + discouraged. Instead, using numpy's + :ref:`dispatch mechanism <basics.dispatch>` is recommended. + +The :class:`ndarray` can be inherited from (in Python or in C) if desired. Therefore, it can form a foundation for many useful classes. Often whether to sub-class the array object or to simply use the core array component as an internal part of a new class is a @@ -147,6 +154,121 @@ NumPy provides several hooks that classes can customize: :func:`__array_prepare__`, :data:`__array_priority__` mechanism described below for ufuncs (which may eventually be deprecated). +.. py:method:: class.__array_function__(func, types, args, kwargs) + + .. versionadded:: 1.16 + + .. note:: + + - In NumPy 1.17, the protocol is enabled by default, but can be disabled + with ``NUMPY_EXPERIMENTAL_ARRAY_FUNCTION=0``. + - In NumPy 1.16, you need to set the environment variable + ``NUMPY_EXPERIMENTAL_ARRAY_FUNCTION=1`` before importing NumPy to use + NumPy function overrides. + - Eventually, expect to ``__array_function__`` to always be enabled. + + - ``func`` is an arbitrary callable exposed by NumPy's public API, + which was called in the form ``func(*args, **kwargs)``. + - ``types`` is a `collection <collections.abc.Collection>`_ + of unique argument types from the original NumPy function call that + implement ``__array_function__``. + - The tuple ``args`` and dict ``kwargs`` are directly passed on from the + original call. + + As a convenience for ``__array_function__`` implementors, ``types`` + provides all argument types with an ``'__array_function__'`` attribute. + This allows implementors to quickly identify cases where they should defer + to ``__array_function__`` implementations on other arguments. + Implementations should not rely on the iteration order of ``types``. + + Most implementations of ``__array_function__`` will start with two + checks: + + 1. Is the given function something that we know how to overload? + 2. Are all arguments of a type that we know how to handle? + + If these conditions hold, ``__array_function__`` should return the result + from calling its implementation for ``func(*args, **kwargs)``. Otherwise, + it should return the sentinel value ``NotImplemented``, indicating that the + function is not implemented by these types. + + There are no general requirements on the return value from + ``__array_function__``, although most sensible implementations should + probably return array(s) with the same type as one of the function's + arguments. + + It may also be convenient to define a custom decorators (``implements`` + below) for registering ``__array_function__`` implementations. + + .. code:: python + + HANDLED_FUNCTIONS = {} + + class MyArray: + def __array_function__(self, func, types, args, kwargs): + if func not in HANDLED_FUNCTIONS: + return NotImplemented + # Note: this allows subclasses that don't override + # __array_function__ to handle MyArray objects + if not all(issubclass(t, MyArray) for t in types): + return NotImplemented + return HANDLED_FUNCTIONS[func](*args, **kwargs) + + def implements(numpy_function): + """Register an __array_function__ implementation for MyArray objects.""" + def decorator(func): + HANDLED_FUNCTIONS[numpy_function] = func + return func + return decorator + + @implements(np.concatenate) + def concatenate(arrays, axis=0, out=None): + ... # implementation of concatenate for MyArray objects + + @implements(np.broadcast_to) + def broadcast_to(array, shape): + ... # implementation of broadcast_to for MyArray objects + + Note that it is not required for ``__array_function__`` implementations to + include *all* of the corresponding NumPy function's optional arguments + (e.g., ``broadcast_to`` above omits the irrelevant ``subok`` argument). + Optional arguments are only passed in to ``__array_function__`` if they + were explicitly used in the NumPy function call. + + Just like the case for builtin special methods like ``__add__``, properly + written ``__array_function__`` methods should always return + ``NotImplemented`` when an unknown type is encountered. Otherwise, it will + be impossible to correctly override NumPy functions from another object + if the operation also includes one of your objects. + + For the most part, the rules for dispatch with ``__array_function__`` + match those for ``__array_ufunc__``. In particular: + + - NumPy will gather implementations of ``__array_function__`` from all + specified inputs and call them in order: subclasses before + superclasses, and otherwise left to right. Note that in some edge cases + involving subclasses, this differs slightly from the + `current behavior <https://bugs.python.org/issue30140>`_ of Python. + - Implementations of ``__array_function__`` indicate that they can + handle the operation by returning any value other than + ``NotImplemented``. + - If all ``__array_function__`` methods return ``NotImplemented``, + NumPy will raise ``TypeError``. + + If no ``__array_function__`` methods exists, NumPy will default to calling + its own implementation, intended for use on NumPy arrays. This case arises, + for example, when all array-like arguments are Python numbers or lists. + (NumPy arrays do have a ``__array_function__`` method, given below, but it + always returns ``NotImplemented`` if any argument other than a NumPy array + subclass implements ``__array_function__``.) + + One deviation from the current behavior of ``__array_ufunc__`` is that + NumPy will only call ``__array_function__`` on the *first* argument of each + unique type. This matches Python's `rule for calling reflected methods + <https://docs.python.org/3/reference/datamodel.html#object.__ror__>`_, and + this ensures that checking overloads has acceptable performance even when + there are a large number of overloaded arguments. + .. py:method:: class.__array_finalize__(obj) This method is called whenever the system internally allocates a diff --git a/doc/source/reference/c-api.ufunc.rst b/doc/source/reference/c-api.ufunc.rst index ba5673cc3..92a679510 100644 --- a/doc/source/reference/c-api.ufunc.rst +++ b/doc/source/reference/c-api.ufunc.rst @@ -49,28 +49,6 @@ Macros Used in universal function code to re-acquire the Python GIL if it was released (because loop->obj was not true). -.. c:function:: UFUNC_CHECK_ERROR(loop) - - A macro used internally to check for errors and goto fail if - found. This macro requires a fail label in the current code - block. The *loop* variable must have at least members (obj, - errormask, and errorobj). If *loop* ->obj is nonzero, then - :c:func:`PyErr_Occurred` () is called (meaning the GIL must be held). If - *loop* ->obj is zero, then if *loop* ->errormask is nonzero, - :c:func:`PyUFunc_checkfperr` is called with arguments *loop* ->errormask - and *loop* ->errobj. If the result of this check of the IEEE - floating point registers is true then the code redirects to the - fail label which must be defined. - -.. c:function:: UFUNC_CHECK_STATUS(ret) - - Deprecated: use npy_clear_floatstatus from npy_math.h instead. - - A macro that expands to platform-dependent code. The *ret* - variable can be any integer. The :c:data:`UFUNC_FPE_{ERR}` bits are - set in *ret* according to the status of the corresponding error - flags of the floating point processor. - Functions --------- diff --git a/doc/source/reference/random/index.rst b/doc/source/reference/random/index.rst index 7de1c838c..fb79e0306 100644 --- a/doc/source/reference/random/index.rst +++ b/doc/source/reference/random/index.rst @@ -73,6 +73,18 @@ See `new-or-different` for more information rg.standard_normal() rg.bit_generator +Something like the following code can be used to support both ``RandomState`` +and ``Generator``, with the understanding that the interfaces are slightly +different + +.. code-block:: python + + try: + rg_integers = rg.integers + except AttributeError: + rg_integers = rg.randint + a = rg_integers(1000) + Seeds can be passed to any of the BitGenerators. The provided value is mixed via `~.SeedSequence` to spread a possible sequence of seeds across a wider range of initialization states for the BitGenerator. Here `~.PCG64` is used and diff --git a/doc/source/user/basics.dispatch.rst b/doc/source/user/basics.dispatch.rst new file mode 100644 index 000000000..f7b8da262 --- /dev/null +++ b/doc/source/user/basics.dispatch.rst @@ -0,0 +1,8 @@ +.. _basics.dispatch: + +******************************* +Writing custom array containers +******************************* + +.. automodule:: numpy.doc.dispatch + diff --git a/doc/source/user/basics.rst b/doc/source/user/basics.rst index 7875aff6e..e0fc0ece3 100644 --- a/doc/source/user/basics.rst +++ b/doc/source/user/basics.rst @@ -12,4 +12,5 @@ NumPy basics basics.broadcasting basics.byteswapping basics.rec + basics.dispatch basics.subclassing diff --git a/numpy/core/function_base.py b/numpy/core/function_base.py index 0dd6ee420..d83af9911 100644 --- a/numpy/core/function_base.py +++ b/numpy/core/function_base.py @@ -3,6 +3,7 @@ from __future__ import division, absolute_import, print_function import functools import warnings import operator +import types from . import numeric as _nx from .numeric import (result_type, NaN, shares_memory, MAY_SHARE_BOUNDS, @@ -430,15 +431,39 @@ def geomspace(start, stop, num=50, endpoint=True, dtype=None, axis=0): return result.astype(dtype, copy=False) -#always succeed -def _add_docstring(obj, doc): +def _needs_add_docstring(obj): + """ + Returns true if the only way to set the docstring of `obj` from python is + via add_docstring. + + This function errs on the side of being overly conservative. + """ + Py_TPFLAGS_HEAPTYPE = 1 << 9 + + if isinstance(obj, (types.FunctionType, types.MethodType, property)): + return False + + if isinstance(obj, type) and obj.__flags__ & Py_TPFLAGS_HEAPTYPE: + return False + + return True + + +def _add_docstring(obj, doc, warn_on_python): + if warn_on_python and not _needs_add_docstring(obj): + warnings.warn( + "add_newdoc was used on a pure-python object {}. " + "Prefer to attach it directly to the source." + .format(obj), + UserWarning, + stacklevel=3) try: add_docstring(obj, doc) except Exception: pass -def add_newdoc(place, obj, doc): +def add_newdoc(place, obj, doc, warn_on_python=True): """ Add documentation to an existing object, typically one defined in C @@ -460,6 +485,9 @@ def add_newdoc(place, obj, doc): If a list, then each element of the list should be a tuple of length two - ``[(method1, docstring1), (method2, docstring2), ...]`` + warn_on_python : bool + If True, the default, emit `UserWarning` if this is used to attach + documentation to a pure-python object. Notes ----- @@ -483,10 +511,10 @@ def add_newdoc(place, obj, doc): """ new = getattr(__import__(place, globals(), {}, [obj]), obj) if isinstance(doc, str): - _add_docstring(new, doc.strip()) + _add_docstring(new, doc.strip(), warn_on_python) elif isinstance(doc, tuple): attr, docstring = doc - _add_docstring(getattr(new, attr), docstring.strip()) + _add_docstring(getattr(new, attr), docstring.strip(), warn_on_python) elif isinstance(doc, list): for attr, docstring in doc: - _add_docstring(getattr(new, attr), docstring.strip()) + _add_docstring(getattr(new, attr), docstring.strip(), warn_on_python) diff --git a/numpy/core/include/numpy/ufuncobject.h b/numpy/core/include/numpy/ufuncobject.h index 15dcdf010..5ff4a0041 100644 --- a/numpy/core/include/numpy/ufuncobject.h +++ b/numpy/core/include/numpy/ufuncobject.h @@ -340,14 +340,6 @@ typedef struct _loop1d_info { #define UFUNC_PYVALS_NAME "UFUNC_PYVALS" -#define UFUNC_CHECK_ERROR(arg) \ - do {if ((((arg)->obj & UFUNC_OBJ_NEEDS_API) && PyErr_Occurred()) || \ - ((arg)->errormask && \ - PyUFunc_checkfperr((arg)->errormask, \ - (arg)->errobj, \ - &(arg)->first))) \ - goto fail;} while (0) - /* * THESE MACROS ARE DEPRECATED. * Use npy_set_floatstatus_* in the npymath library. @@ -357,10 +349,6 @@ typedef struct _loop1d_info { #define UFUNC_FPE_UNDERFLOW NPY_FPE_UNDERFLOW #define UFUNC_FPE_INVALID NPY_FPE_INVALID -#define UFUNC_CHECK_STATUS(ret) \ - { \ - ret = npy_clear_floatstatus(); \ - } #define generate_divbyzero_error() npy_set_floatstatus_divbyzero() #define generate_overflow_error() npy_set_floatstatus_overflow() diff --git a/numpy/core/src/multiarray/arraytypes.c.src b/numpy/core/src/multiarray/arraytypes.c.src index c586a0b1d..5d9e990e8 100644 --- a/numpy/core/src/multiarray/arraytypes.c.src +++ b/numpy/core/src/multiarray/arraytypes.c.src @@ -221,9 +221,7 @@ static int if (PySequence_NoString_Check(op)) { PyErr_SetString(PyExc_ValueError, "setting an array element with a sequence."); - Py_DECREF(type); - Py_XDECREF(value); - Py_XDECREF(traceback); + npy_PyErr_ChainExceptionsCause(type, value, traceback); } else { PyErr_Restore(type, value, traceback); diff --git a/numpy/core/src/multiarray/ctors.c b/numpy/core/src/multiarray/ctors.c index c17266251..53efb1cea 100644 --- a/numpy/core/src/multiarray/ctors.c +++ b/numpy/core/src/multiarray/ctors.c @@ -1849,6 +1849,13 @@ PyArray_GetArrayParamsFromObject(PyObject *op, *out_arr = NULL; return 0; } + if (is_object && (requested_dtype != NULL) && + (requested_dtype->type_num != NPY_OBJECT)) { + PyErr_SetString(PyExc_ValueError, + "cannot create an array from unequal-length (ragged) sequences"); + Py_DECREF(*out_dtype); + return -1; + } /* If object arrays are forced */ if (is_object) { Py_DECREF(*out_dtype); diff --git a/numpy/core/tests/test_multiarray.py b/numpy/core/tests/test_multiarray.py index 3a5a5b939..53e538f7d 100644 --- a/numpy/core/tests/test_multiarray.py +++ b/numpy/core/tests/test_multiarray.py @@ -44,7 +44,7 @@ from numpy.testing import ( assert_, assert_raises, assert_warns, assert_equal, assert_almost_equal, assert_array_equal, assert_raises_regex, assert_array_almost_equal, assert_allclose, IS_PYPY, HAS_REFCOUNT, assert_array_less, runstring, - temppath, suppress_warnings, break_cycles, + temppath, suppress_warnings, break_cycles, assert_raises_regex, ) from numpy.core.tests._locales import CommaDecimalPointLocale @@ -497,6 +497,9 @@ class TestArrayConstruction(object): assert_(np.ascontiguousarray(d).flags.c_contiguous) assert_(np.asfortranarray(d).flags.f_contiguous) + def test_ragged(self): + assert_raises_regex(ValueError, 'ragged', + np.array, [[1], [2, 3]], dtype=int) class TestAssignment(object): def test_assignment_broadcasting(self): diff --git a/numpy/distutils/system_info.py b/numpy/distutils/system_info.py index ba2b1f46c..2ff0ba7b3 100644 --- a/numpy/distutils/system_info.py +++ b/numpy/distutils/system_info.py @@ -1905,8 +1905,6 @@ class blas_info(system_info): library_dirs=info['library_dirs'], extra_postargs=info.get('extra_link_args', [])) return libs - # This breaks the for loop - break except distutils.ccompiler.LinkError: pass finally: diff --git a/numpy/doc/dispatch.py b/numpy/doc/dispatch.py new file mode 100644 index 000000000..09a3e5134 --- /dev/null +++ b/numpy/doc/dispatch.py @@ -0,0 +1,271 @@ +""".. _dispatch_mechanism: + +Numpy's dispatch mechanism, introduced in numpy version v1.16 is the +recommended approach for writing custom N-dimensional array containers that are +compatible with the numpy API and provide custom implementations of numpy +functionality. Applications include `dask <http://dask.pydata.org>`_ arrays, an +N-dimensional array distributed across multiple nodes, and `cupy +<https://docs-cupy.chainer.org/en/stable/>`_ arrays, an N-dimensional array on +a GPU. + +To get a feel for writing custom array containers, we'll begin with a simple +example that has rather narrow utility but illustrates the concepts involved. + +>>> import numpy as np +>>> class DiagonalArray: +... def __init__(self, N, value): +... self._N = N +... self._i = value +... def __repr__(self): +... return f"{self.__class__.__name__}(N={self._N}, value={self._i})" +... def __array__(self): +... return self._i * np.eye(self._N) +... + +Our custom array can be instantiated like: + +>>> arr = DiagonalArray(5, 1) +>>> arr +DiagonalArray(N=5, value=1) + +We can convert to a numpy array using :func:`numpy.array` or +:func:`numpy.asarray`, which will call its ``__array__`` method to obtain a +standard ``numpy.ndarray``. + +>>> np.asarray(arr) +array([[1., 0., 0., 0., 0.], + [0., 1., 0., 0., 0.], + [0., 0., 1., 0., 0.], + [0., 0., 0., 1., 0.], + [0., 0., 0., 0., 1.]]) + +If we operate on ``arr`` with a numpy function, numpy will again use the +``__array__`` interface to convert it to an array and then apply the function +in the usual way. + +>>> np.multiply(arr, 2) +array([[2., 0., 0., 0., 0.], + [0., 2., 0., 0., 0.], + [0., 0., 2., 0., 0.], + [0., 0., 0., 2., 0.], + [0., 0., 0., 0., 2.]]) + + +Notice that the return type is a standard ``numpy.ndarray``. + +>>> type(arr) +numpy.ndarray + +How can we pass our custom array type through this function? Numpy allows a +class to indicate that it would like to handle computations in a custom-defined +way through the interaces ``__array_ufunc__`` and ``__array_function__``. Let's +take one at a time, starting with ``_array_ufunc__``. This method covers +:ref:`ufuncs`, a class of functions that includes, for example, +:func:`numpy.multiply` and :func:`numpy.sin`. + +The ``__array_ufunc__`` receives: + +- ``ufunc``, a function like ``numpy.multiply`` +- ``method``, a string, differentiating between ``numpy.multiply(...)`` and + variants like ``numpy.multiply.outer``, ``numpy.multiply.accumulate``, and so + on. For the common case, ``numpy.multiply(...)``, ``method == '__call__'``. +- ``inputs``, which could be a mixture of different types +- ``kwargs``, keyword arguments passed to the function + +For this example we will only handle the method ``'__call__``. + +>>> from numbers import Number +>>> class DiagonalArray: +... def __init__(self, N, value): +... self._N = N +... self._i = value +... def __repr__(self): +... return f"{self.__class__.__name__}(N={self._N}, value={self._i})" +... def __array__(self): +... return self._i * np.eye(self._N) +... def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): +... if method == '__call__': +... N = None +... scalars = [] +... for input in inputs: +... if isinstance(input, Number): +... scalars.append(input) +... elif isinstance(input, self.__class__): +... scalars.append(input._i) +... if N is not None: +... if N != self._N: +... raise TypeError("inconsistent sizes") +... else: +... N = self._N +... else: +... return NotImplemented +... return self.__class__(N, ufunc(*scalars, **kwargs)) +... else: +... return NotImplemented +... + +Now our custom array type passes through numpy functions. + +>>> arr = DiagonalArray(5, 1) +>>> np.multiply(arr, 3) +DiagonalArray(N=5, value=3) +>>> np.add(arr, 3) +DiagonalArray(N=5, value=4) +>>> np.sin(arr) +DiagonalArray(N=5, value=0.8414709848078965) + +At this point ``arr + 3`` does not work. + +>>> arr + 3 +TypeError: unsupported operand type(s) for *: 'DiagonalArray' and 'int' + +To support it, we need to define the Python interfaces ``__add__``, ``__lt__``, +and so on to dispatch to the corresponding ufunc. We can achieve this +conveniently by inheriting from the mixin +:class:`~numpy.lib.mixins.NDArrayOperatorsMixin`. + +>>> import numpy.lib.mixins +>>> class DiagonalArray(numpy.lib.mixins.NDArrayOperatorsMixin): +... def __init__(self, N, value): +... self._N = N +... self._i = value +... def __repr__(self): +... return f"{self.__class__.__name__}(N={self._N}, value={self._i})" +... def __array__(self): +... return self._i * np.eye(self._N) +... def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): +... if method == '__call__': +... N = None +... scalars = [] +... for input in inputs: +... if isinstance(input, Number): +... scalars.append(input) +... elif isinstance(input, self.__class__): +... scalars.append(input._i) +... if N is not None: +... if N != self._N: +... raise TypeError("inconsistent sizes") +... else: +... N = self._N +... else: +... return NotImplemented +... return self.__class__(N, ufunc(*scalars, **kwargs)) +... else: +... return NotImplemented +... + +>>> arr = DiagonalArray(5, 1) +>>> arr + 3 +DiagonalArray(N=5, value=4) +>>> arr > 0 +DiagonalArray(N=5, value=True) + +Now let's tackle ``__array_function__``. We'll create dict that maps numpy +functions to our custom variants. + +>>> HANDLED_FUNCTIONS = {} +>>> class DiagonalArray(numpy.lib.mixins.NDArrayOperatorsMixin): +... def __init__(self, N, value): +... self._N = N +... self._i = value +... def __repr__(self): +... return f"{self.__class__.__name__}(N={self._N}, value={self._i})" +... def __array__(self): +... return self._i * np.eye(self._N) +... def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): +... if method == '__call__': +... N = None +... scalars = [] +... for input in inputs: +... # In this case we accept only scalar numbers or DiagonalArrays. +... if isinstance(input, Number): +... scalars.append(input) +... elif isinstance(input, self.__class__): +... scalars.append(input._i) +... if N is not None: +... if N != self._N: +... raise TypeError("inconsistent sizes") +... else: +... N = self._N +... else: +... return NotImplemented +... return self.__class__(N, ufunc(*scalars, **kwargs)) +... else: +... return NotImplemented +... def __array_function__(self, func, types, args, kwargs): +... if func not in HANDLED_FUNCTIONS: +... return NotImplemented +... # Note: this allows subclasses that don't override +... # __array_function__ to handle DiagonalArray objects. +... if not all(issubclass(t, self.__class__) for t in types): +... return NotImplemented +... return HANDLED_FUNCTIONS[func](*args, **kwargs) +... + +A convenient pattern is to define a decorator ``implements`` that can be used +to add functions to ``HANDLED_FUNCTIONS``. + +>>> def implements(np_function): +... "Register an __array_function__ implementation for DiagonalArray objects." +... def decorator(func): +... HANDLED_FUNCTIONS[np_function] = func +... return func +... return decorator +... + +Now we write implementations of numpy functions for ``DiagonalArray``. +For completeness, to support the usage ``arr.sum()`` add a method ``sum`` that +calls ``numpy.sum(self)``, and the same for ``mean``. + +>>> @implements(np.sum) +... def sum(a): +... "Implementation of np.sum for DiagonalArray objects" +... return arr._i * arr._N +... +>>> @implements(np.mean) +... def sum(a): +... "Implementation of np.mean for DiagonalArray objects" +... return arr._i / arr._N +... +>>> arr = DiagonalArray(5, 1) +>>> np.sum(arr) +5 +>>> np.mean(arr) +0.2 + +If the user tries to use any numpy functions not included in +``HANDLED_FUNCTIONS``, a ``TypeError`` will be raised by numpy, indicating that +this operation is not supported. For example, concatenating two +``DiagonalArrays`` does not produce another diagonal array, so it is not +supported. + +>>> np.concatenate([arr, arr]) +TypeError: no implementation found for 'numpy.concatenate' on types that implement __array_function__: [<class '__main__.DiagonalArray'>] + +Additionally, our implementations of ``sum`` and ``mean`` do not accept the +optional arguments that numpy's implementation does. + +>>> np.sum(arr, axis=0) +TypeError: sum() got an unexpected keyword argument 'axis' + +The user always has the option of converting to a normal ``numpy.ndarray`` with +:func:`numpy.asarray` and using standard numpy from there. + +>>> np.concatenate([np.asarray(arr), np.asarray(arr)]) +array([[1., 0., 0., 0., 0.], + [0., 1., 0., 0., 0.], + [0., 0., 1., 0., 0.], + [0., 0., 0., 1., 0.], + [0., 0., 0., 0., 1.], + [1., 0., 0., 0., 0.], + [0., 1., 0., 0., 0.], + [0., 0., 1., 0., 0.], + [0., 0., 0., 1., 0.], + [0., 0., 0., 0., 1.]]) + +Refer to the `dask source code <https://github.com/dask/dask>`_ and +`cupy source code <https://github.com/cupy/cupy>`_ for more fully-worked +examples of custom array containers. + +See also `NEP 18 <http://www.numpy.org/neps/nep-0018-array-function-protocol.html>`_. +""" diff --git a/numpy/fft/pocketfft.py b/numpy/fft/pocketfft.py index 45dc162f6..b7f6f1434 100644 --- a/numpy/fft/pocketfft.py +++ b/numpy/fft/pocketfft.py @@ -392,8 +392,9 @@ def irfft(a, n=None, axis=-1, norm=None): Length of the transformed axis of the output. For `n` output points, ``n//2+1`` input points are necessary. If the input is longer than this, it is cropped. If it is shorter than this, - it is padded with zeros. If `n` is not given, it is determined from - the length of the input along the axis specified by `axis`. + it is padded with zeros. If `n` is not given, it is taken to be + ``2*(m-1)`` where ``m`` is the length of the input along the axis + specified by `axis`. axis : int, optional Axis over which to compute the inverse FFT. If not given, the last axis is used. @@ -436,6 +437,14 @@ def irfft(a, n=None, axis=-1, norm=None): thus resample a series to `m` points via Fourier interpolation by: ``a_resamp = irfft(rfft(a), m)``. + The correct interpretation of the hermitian input depends on the length of + the original data, as given by `n`. This is because each input shape could + correspond to either an odd or even length signal. By default, `irfft` + assumes an even output length which puts the last entry at the Nyquist + frequency; aliasing with its symmetric counterpart. By Hermitian symmetry, + the value is thus treated as purely real. To avoid losing information, the + correct length of the real input **must** be given. + Examples -------- >>> np.fft.ifft([1, -1j, -1, 1j]) @@ -473,8 +482,9 @@ def hfft(a, n=None, axis=-1, norm=None): Length of the transformed axis of the output. For `n` output points, ``n//2 + 1`` input points are necessary. If the input is longer than this, it is cropped. If it is shorter than this, it is - padded with zeros. If `n` is not given, it is determined from the - length of the input along the axis specified by `axis`. + padded with zeros. If `n` is not given, it is taken to be ``2*(m-1)`` + where ``m`` is the length of the input along the axis specified by + `axis`. axis : int, optional Axis over which to compute the FFT. If not given, the last axis is used. @@ -513,6 +523,14 @@ def hfft(a, n=None, axis=-1, norm=None): * even: ``ihfft(hfft(a, 2*len(a) - 2) == a``, within roundoff error, * odd: ``ihfft(hfft(a, 2*len(a) - 1) == a``, within roundoff error. + The correct interpretation of the hermitian input depends on the length of + the original data, as given by `n`. This is because each input shape could + correspond to either an odd or even length signal. By default, `hfft` + assumes an even output length which puts the last entry at the Nyquist + frequency; aliasing with its symmetric counterpart. By Hermitian symmetry, + the value is thus treated as purely real. To avoid losing information, the + shape of the full signal **must** be given. + Examples -------- >>> signal = np.array([1, 2, 3, 4, 3, 2]) @@ -1167,8 +1185,9 @@ def irfftn(a, s=None, axes=None, norm=None): where ``s[-1]//2+1`` points of the input are used. Along any axis, if the shape indicated by `s` is smaller than that of the input, the input is cropped. If it is larger, the input is padded - with zeros. If `s` is not given, the shape of the input along the - axes specified by `axes` is used. + with zeros. If `s` is not given, the shape of the input along the axes + specified by axes is used. Except for the last axis which is taken to be + ``2*(m-1)`` where ``m`` is the length of the input along that axis. axes : sequence of ints, optional Axes over which to compute the inverse FFT. If not given, the last `len(s)` axes are used, or all axes if `s` is also not specified. @@ -1213,6 +1232,15 @@ def irfftn(a, s=None, axes=None, norm=None): See `rfft` for definitions and conventions used for real input. + The correct interpretation of the hermitian input depends on the shape of + the original data, as given by `s`. This is because each input shape could + correspond to either an odd or even length signal. By default, `irfftn` + assumes an even output length which puts the last entry at the Nyquist + frequency; aliasing with its symmetric counterpart. When performing the + final complex to real transform, the last value is thus treated as purely + real. To avoid losing information, the correct shape of the real input + **must** be given. + Examples -------- >>> a = np.zeros((3, 2, 2)) @@ -1244,7 +1272,7 @@ def irfft2(a, s=None, axes=(-2, -1), norm=None): a : array_like The input array s : sequence of ints, optional - Shape of the inverse FFT. + Shape of the real output to the inverse FFT. axes : sequence of ints, optional The axes over which to compute the inverse fft. Default is the last two axes. diff --git a/numpy/polynomial/polyutils.py b/numpy/polynomial/polyutils.py index e9fbc0fcf..a9059f522 100644 --- a/numpy/polynomial/polyutils.py +++ b/numpy/polynomial/polyutils.py @@ -46,6 +46,7 @@ Functions from __future__ import division, absolute_import, print_function import operator +import warnings import numpy as np diff --git a/numpy/polynomial/tests/test_classes.py b/numpy/polynomial/tests/test_classes.py index 15e24f92b..2261f960b 100644 --- a/numpy/polynomial/tests/test_classes.py +++ b/numpy/polynomial/tests/test_classes.py @@ -16,7 +16,7 @@ from numpy.testing import ( assert_almost_equal, assert_raises, assert_equal, assert_, ) from numpy.compat import long - +from numpy.polynomial.polyutils import RankWarning # # fixtures @@ -133,6 +133,17 @@ def test_fromroots(Poly): assert_almost_equal(p2.coef[-1], 1) +def test_bad_conditioned_fit(Poly): + + x = [0., 0., 1.] + y = [1., 2., 3.] + + # check RankWarning is raised + with pytest.warns(RankWarning) as record: + Poly.fit(x, y, 2) + assert record[0].message.args[0] == "The fit may be poorly conditioned" + + def test_fit(Poly): def f(x): diff --git a/numpy/polynomial/tests/test_polynomial.py b/numpy/polynomial/tests/test_polynomial.py index 08b73da15..1436963c6 100644 --- a/numpy/polynomial/tests/test_polynomial.py +++ b/numpy/polynomial/tests/test_polynomial.py @@ -9,7 +9,7 @@ import numpy as np import numpy.polynomial.polynomial as poly from numpy.testing import ( assert_almost_equal, assert_raises, assert_equal, assert_, - assert_array_equal) + assert_warns, assert_array_equal) def trim(x): @@ -297,6 +297,8 @@ class TestIntegral(object): assert_raises(ValueError, poly.polyint, [0], lbnd=[0]) assert_raises(ValueError, poly.polyint, [0], scl=[0]) assert_raises(TypeError, poly.polyint, [0], axis=.5) + with assert_warns(DeprecationWarning): + poly.polyint([1, 1], 1.) # test integration of zero polynomial for i in range(2, 5): diff --git a/tools/npy_tempita/__init__.py b/tools/npy_tempita/__init__.py index dfb40e965..f75f23a21 100644 --- a/tools/npy_tempita/__init__.py +++ b/tools/npy_tempita/__init__.py @@ -105,21 +105,21 @@ class Template(object): def __init__(self, content, name=None, namespace=None, stacklevel=None, get_template=None, default_inherit=None, line_offset=0, - delimeters=None): + delimiters=None): self.content = content - # set delimeters - if delimeters is None: - delimeters = (self.default_namespace['start_braces'], + # set delimiters + if delimiters is None: + delimiters = (self.default_namespace['start_braces'], self.default_namespace['end_braces']) else: - assert len(delimeters) == 2 and all( - [isinstance(delimeter, basestring_) - for delimeter in delimeters]) + assert len(delimiters) == 2 and all( + [isinstance(delimiter, basestring_) + for delimiter in delimiters]) self.default_namespace = self.__class__.default_namespace.copy() - self.default_namespace['start_braces'] = delimeters[0] - self.default_namespace['end_braces'] = delimeters[1] - self.delimeters = delimeters + self.default_namespace['start_braces'] = delimiters[0] + self.default_namespace['end_braces'] = delimiters[1] + self.delimiters = delimiters self._unicode = is_unicode(content) if name is None and stacklevel is not None: @@ -143,7 +143,7 @@ class Template(object): self.name = name self._parsed = parse( content, name=name, line_offset=line_offset, - delimeters=self.delimeters) + delimiters=self.delimiters) if namespace is None: namespace = {} self.namespace = namespace @@ -392,9 +392,9 @@ class Template(object): return msg -def sub(content, delimeters=None, **kw): +def sub(content, delimiters=None, **kw): name = kw.get('__name') - tmpl = Template(content, name=name, delimeters=delimeters) + tmpl = Template(content, name=name, delimiters=delimiters) return tmpl.substitute(kw) @@ -652,28 +652,28 @@ del _Empty ############################################################ -def lex(s, name=None, trim_whitespace=True, line_offset=0, delimeters=None): - if delimeters is None: - delimeters = (Template.default_namespace['start_braces'], +def lex(s, name=None, trim_whitespace=True, line_offset=0, delimiters=None): + if delimiters is None: + delimiters = (Template.default_namespace['start_braces'], Template.default_namespace['end_braces']) in_expr = False chunks = [] last = 0 last_pos = (line_offset + 1, 1) - token_re = re.compile(r'%s|%s' % (re.escape(delimeters[0]), - re.escape(delimeters[1]))) + token_re = re.compile(r'%s|%s' % (re.escape(delimiters[0]), + re.escape(delimiters[1]))) for match in token_re.finditer(s): expr = match.group(0) pos = find_position(s, match.end(), last, last_pos) - if expr == delimeters[0] and in_expr: - raise TemplateError('%s inside expression' % delimeters[0], + if expr == delimiters[0] and in_expr: + raise TemplateError('%s inside expression' % delimiters[0], position=pos, name=name) - elif expr == delimeters[1] and not in_expr: - raise TemplateError('%s outside expression' % delimeters[1], + elif expr == delimiters[1] and not in_expr: + raise TemplateError('%s outside expression' % delimiters[1], position=pos, name=name) - if expr == delimeters[0]: + if expr == delimiters[0]: part = s[last:match.start()] if part: chunks.append(part) @@ -684,7 +684,7 @@ def lex(s, name=None, trim_whitespace=True, line_offset=0, delimeters=None): last = match.end() last_pos = pos if in_expr: - raise TemplateError('No %s to finish last expression' % delimeters[1], + raise TemplateError('No %s to finish last expression' % delimiters[1], name=name, position=last_pos) part = s[last:] if part: @@ -822,12 +822,12 @@ def find_position(string, index, last_index, last_pos): return (last_pos[0] + lines, column) -def parse(s, name=None, line_offset=0, delimeters=None): +def parse(s, name=None, line_offset=0, delimiters=None): - if delimeters is None: - delimeters = (Template.default_namespace['start_braces'], + if delimiters is None: + delimiters = (Template.default_namespace['start_braces'], Template.default_namespace['end_braces']) - tokens = lex(s, name=name, line_offset=line_offset, delimeters=delimeters) + tokens = lex(s, name=name, line_offset=line_offset, delimiters=delimiters) result = [] while tokens: next_chunk, tokens = parse_expr(tokens, name) |