CofeehousePy/deps/numpy/doc/neps/nep-0020-gufunc-signature-e...

258 lines
14 KiB
ReStructuredText

===============================================================
NEP 20 — Expansion of Generalized Universal Function Signatures
===============================================================
:Author: Marten van Kerkwijk <mhvk@astro.utoronto.ca>
:Status: Final
:Type: Standards Track
:Created: 2018-06-10
:Resolution: https://mail.python.org/pipermail/numpy-discussion/2018-April/077959.html,
https://mail.python.org/pipermail/numpy-discussion/2018-May/078078.html
.. note:: The proposal to add fixed (i) and flexible (ii) dimensions
was accepted, while that to add broadcastable (iii) ones was deferred.
Abstract
--------
Generalized universal functions are, as their name indicates, generalization
of universal functions: they operate on non-scalar elements. Their signature
describes the structure of the elements they operate on, with names linking
dimensions of the operands that should be the same. Here, it is proposed to
extend the signature to allow the signature to indicate that a dimension (i)
has fixed size; (ii) can be absent; and (iii) can be broadcast.
Detailed description
--------------------
Each part of the proposal is driven by specific needs [1]_.
1. Fixed-size dimensions. Code working with spatial vectors often explicitly
is for 2 or 3-dimensional space (e.g., the code from the `Standards Of
Fundamental Astronomy <http://www.iausofa.org/>`_, which the author hopes
to wrap using gufuncs for astropy [2]_). The signature should be able to
indicate that. E.g., the signature of a function that converts a polar
angle to a two-dimensional cartesian unit vector would currently have to be
``()->(n)``, with there being no way to indicate that ``n`` has to equal 2.
Indeed, this signature is particularly annoying since without putting in an
output argument, the current gufunc wrapper code fails because it cannot
determine ``n``. Similarly, the signature for an cross product of two
3-dimensional vectors has to be ``(n),(n)->(n)``, with again no way to
indicate that ``n`` has to equal 3. Hence, the proposal here to allow one
to give numerical values in addition to variable names. Thus, angle to
two-dimensional unit vector would be ``()->(2)``; two angles to
three-dimensional unit vector ``(),()->(3)``; and that for the cross
product of two three-dimensional vectors would be ``(3),(3)->(3)``.
2. Possibly missing dimensions. This part is almost entirely driven by the
wish to wrap ``matmul`` in a gufunc. ``matmul`` stands for matrix
multiplication, and if it did only that, it could be covered with the
signature ``(m,n),(n,p)->(m,p)``. However, it has special cases for when a
dimension is missing, allowing either argument to be treated as a single
vector, with the function thus becoming, effectively, vector-matrix,
matrix-vector, or vector-vector multiplication (but with no
broadcasting). To support this, it is suggested to allow postfixing a
dimension name with a question mark to indicate that the dimension does not
necessarily have to be present.
With this addition, the signature for ``matmul`` can be expressed as
``(m?,n),(n,p?)->(m?,p?)``. This indicates that if, e.g., the second
operand has only one dimension, for the purposes of the elementary function
it will be treated as if that input has core shape ``(n, 1)``, and the
output has the corresponding core shape of ``(m, 1)``. The actual output
array, however, has the flexible dimension removed, i.e., it will have
shape ``(..., m)``. Similarly, if both arguments have only a single
dimension, the inputs will be presented as having shapes ``(1, n)`` and
``(n, 1)`` to the elementary function, and the output as ``(1, 1)``, while
the actual output array returned will have shape ``()``. In this way, the
signature allows one to use a single elementary function for four related
but different signatures, ``(m,n),(n,p)->(m,p)``, ``(n),(n,p)->(p)``,
``(m,n),(n)->(m)`` and ``(n),(n)->()``.
3. Dimensions that can be broadcast. For some applications, broadcasting
between operands makes sense. For instance, an ``all_equal`` function that
compares vectors in arrays could have a signature ``(n),(n)->()``, but this
forces both operands to be arrays, while it would be useful also to check
that, e.g., all parts of a vector are constant (maybe zero). The proposal
is to allow the implementer of a gufunc to indicate that a dimension can be
broadcast by post-fixing the dimension name with ``|1``. Hence, the
signature for ``all_equal`` would become ``(n|1),(n|1)->()``. The
signature seems handy more generally for "chained ufuncs"; e.g., another
application might be in a putative ufunc implementing ``sumproduct``.
Another example that arose in the discussion, is of a weighted mean, which
might look like ``weighted_mean(y, sigma[, axis, ...])``, returning the
mean and its uncertainty. With a signature of ``(n),(n)->(),()``, one
would be forced to always give as many sigmas as there are data points,
while broadcasting would allow one to give a single sigma for all points
(which is still useful to calculate the uncertainty on the mean).
Implementation
--------------
The proposed changes have all been implemented [3]_, [4]_, [5]_. These PRs
extend the ufunc structure with two new fields, each of size equal to the
number of distinct dimensions, with ``core_dim_sizes`` holding possibly fixed
sizes, and ``core_dim_flags`` holding flags indicating whether a dimension can
be missing or broadcast. To ensure we can distinguish between this new
version and previous versions, an unused entry ``reserved1`` is repurposed as
a version number.
In the implementation, care is taken that to the elementary function flagged
dimensions are not treated any differently than non-flagged ones: for
instance, sizes of fixed-size dimensions are still passed on to the elementary
function (but the loop can now count on that size being equal to the fixed one
given in the signature).
An implementation detail to be decided upon is whether it might be handy to
have a summary of all flags. This could possibly be stored in ``core_enabled``
(which currently is a bool), with non-zero continuing to indicate a gufunc,
but specific flags indicating whether or not a gufunc uses fixed, flexible, or
broadcastable dimensions.
With the above, the formal defition of the syntax would become [4]_::
<Signature> ::= <Input arguments> "->" <Output arguments>
<Input arguments> ::= <Argument list>
<Output arguments> ::= <Argument list>
<Argument list> ::= nil | <Argument> | <Argument> "," <Argument list>
<Argument> ::= "(" <Core dimension list> ")"
<Core dimension list> ::= nil | <Core dimension> |
<Core dimension> "," <Core dimension list>
<Core dimension> ::= <Dimension name> <Dimension modifier>
<Dimension name> ::= valid Python variable name | valid integer
<Dimension modifier> ::= nil | "|1" | "?"
#. All quotes are for clarity.
#. Unmodified core dimensions that share the same name must have the same size.
Each dimension name typically corresponds to one level of looping in the
elementary function's implementation.
#. White spaces are ignored.
#. An integer as a dimension name freezes that dimension to the value.
#. If a name if suffixed with the ``|1`` modifier, it is allowed to broadcast
against other dimensions with the same name. All input dimensions
must share this modifier, while no output dimensions should have it.
#. If the name is suffixed with the ``?`` modifier, the dimension is a core
dimension only if it exists on all inputs and outputs that share it;
otherwise it is ignored (and replaced by a dimension of size 1 for the
elementary function).
Examples of signatures [4]_:
+----------------------------+-----------------------------------+
| Signature | Possible use |
+----------------------------+-----------------------------------+
| ``(),()->()`` | Addition |
+----------------------------+-----------------------------------+
| ``(i)->()`` | Sum over last axis |
+----------------------------+-----------------------------------+
| ``(i|1),(i|1)->()`` | Test for equality along axis, |
| | allowing comparison with a scalar |
+----------------------------+-----------------------------------+
| ``(i),(i)->()`` | inner vector product |
+----------------------------+-----------------------------------+
| ``(m,n),(n,p)->(m,p)`` | matrix multiplication |
+----------------------------+-----------------------------------+
| ``(n),(n,p)->(p)`` | vector-matrix multiplication |
+----------------------------+-----------------------------------+
| ``(m,n),(n)->(m)`` | matrix-vector multiplication |
+----------------------------+-----------------------------------+
| ``(m?,n),(n,p?)->(m?,p?)`` | all four of the above at once, |
| | except vectors cannot have loop |
| | dimensions (ie, like ``matmul``) |
+----------------------------+-----------------------------------+
| ``(3),(3)->(3)`` | cross product for 3-vectors |
+----------------------------+-----------------------------------+
| ``(i,t),(j,t)->(i,j)`` | inner over the last dimension, |
| | outer over the second to last, |
| | and loop/broadcast over the rest. |
+----------------------------+-----------------------------------+
Backward compatibility
----------------------
One possible worry is the change in ufunc structure. For most applications,
which call ``PyUFunc_FromDataAndSignature``, this is entirely transparent.
Furthermore, by repurposing ``reserved1`` as a version number, code compiled
against older versions of numpy will continue to work (though one will get a
warning upon import of that code with a newer version of numpy), except if
code explicitly changes the ``reserved1`` entry.
Alternatives
------------
It was suggested instead of extending the signature, to have multiple
dispatch, so that, e.g., ``matmul`` would simply have the multiple signatures
it supports, i.e., instead of ``(m?,n),(n,p?)->(m?,p?)`` one would have
``(m,n),(n,p)->(m,p) | (n),(n,p)->(p) | (m,n),(n)->(m) | (n),(n)->()``. A
disadvantage of this is that the developer now has to make sure that the
elementary function can deal with these different signatures. Furthermore,
the expansion quickly becomes cumbersome. For instance, for the ``all_equal``
signature of ``(n|1),(n|1)->()``, one would have to have five entries:
``(n),(n)->() | (n),(1)->() | (1),(n)->() | (n),()->() | (),(n)->()``. For
signatures like ``(m|1,n|1,o|1),(m|1,n|1,o|1)->()`` (from the ``cube_equal``
test case in [4]_), it is not even worth writing out the expansion.
For broadcasting, the alternative suffix of ``^`` was suggested (as
broadcasting can be thought of as increasing the size of the array). This
seems less clear. Furthermore, it was wondered whether it should not just be
an all-or-nothing flag. This could be the case, though given the postfix
for flexible dimensions, arguably another postfix is clearer (as is the
implementation).
Discussion
----------
The proposals here were discussed at fair length on the mailing list [6]_,
[7]_. The main points of contention were whether the use cases were
sufficiently strong. In particular, for frozen dimensions, it was argued that
checks on the right number could be put in loop selection code. This seems
much less clear for no benefit.
For broadcasting, the lack of examples of elementary functions that might need
it was noted, with it being questioned whether something like ``all_equal``
was best done with a gufunc rather than as a special method on ``np.equal``.
One counter-argument to this would be that there is an actual PR for
``all_equal`` [8]_. Another that even if one were to use a method, it would
be good to be able to express their signature (just as is possible at least
for ``reduce`` and ``accumulate``).
A final argument was that we were making the gufuncs too complex. This
arguably holds for the dimensions that can be omitted, but that also has the
strongest use case. The frozen dimensions has a very simple implementation and
its meaning is obvious. The ability to broadcast is simple too, once the
flexible dimensions are supported.
References and Footnotes
------------------------
.. [1] Identified needs and suggestions for the implementation are not all by
the author. In particular, the suggestion for fixed dimensions and
initial implementation was by Jaime Frio (`gh-5015
<https://github.com/numpy/numpy/pull/5015>`_), the suggestion of ``?``
to indicate dimensions can be omitted was by Nathaniel Smith, and the
initial implementation of that by Matti Picus (`gh-11132
<https://github.com/numpy/numpy/pull/11132>`_).
.. [2] `wrap ERFA functions in gufuncs
<https://github.com/astropy/astropy/pull/7502>`_ (`ERFA
<https://github.com/liberfa/erfa>`_) is the less stringently licensed
version of `Standards Of Fundamental Astronomy
<http://www.iausofa.org/>`_
.. [3] `fixed-size and flexible dimensions
<https://github.com/numpy/numpy/pull/11175>`_
.. [4] `broadcastable dimensions
<https://github.com/numpy/numpy/pull/11179>`_
.. [5] `use in matmul <https://github.com/numpy/numpy/pull/11133>`_
.. [6] Discusses implementations for ``matmul``:
https://mail.python.org/pipermail/numpy-discussion/2018-May/077972.html,
https://mail.python.org/pipermail/numpy-discussion/2018-May/078021.html
.. [7] Broadcasting:
https://mail.python.org/pipermail/numpy-discussion/2018-May/078078.html
.. [8] `Logical gufuncs <https://github.com/numpy/numpy/pull/8528>`_ (includes
``all_equal``)
Copyright
---------
This document has been placed in the public domain.