Skip to content

Conversation

nschloe
Copy link
Contributor

@nschloe nschloe commented Apr 14, 2021

With a view on numpy/numpy#10615, I checked where in SciPy there are implicit array-to-float conversions. In all cases I've checked so far, there was some slight carelessness in the past that we probably have to maintain for backwards compatibility. For example, we allow return values from optimization objective function to be of any shape, as long as they are size 1. (The strict -- some would argue more correct -- approach would be to enforce true scalar return values.)

Anyway, here is a PR that fixes some of the implicit array-to-float conversions by making them explicit. These fix ~30 of the ~90 test warnings when the deprecation in numpy/numpy#10615 is enabled. They also give good error messages if the user returns something other than a size-1 array from an objective function rather than failing at an assignment, so I think that's a win, too.

Let me know what you think.

@nschloe nschloe requested a review from andyfaff as a code owner April 14, 2021 16:26
@tirthasheshpatel tirthasheshpatel added the maintenance Items related to regular maintenance tasks label Apr 14, 2021
Copy link
Member

@tirthasheshpatel tirthasheshpatel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes look good but how about using .item() instead of .flat[0] for cases when the array can have multiple dimensions but one item? IMO, the former looks cleaner and preserves the behaviour of returning a Python float (compared to .flat[0] which returns NumPy floats)

@nschloe
Copy link
Contributor Author

nschloe commented Apr 14, 2021

Changes look good but how about using .item() instead of .flat[0]

Sure, looks cleaner indeed. Let me make the changes.

@nschloe
Copy link
Contributor Author

nschloe commented Apr 14, 2021

Done.

@tirthasheshpatel
Copy link
Member

tirthasheshpatel commented Apr 14, 2021

@nschloe Thanks! The new error messages need tests. Can you please add them too?

@nschloe nschloe marked this pull request as draft April 15, 2021 08:20
@nschloe
Copy link
Contributor Author

nschloe commented Apr 15, 2021

Instead of fiddling around with the individual optimization methods, let me try and edit the function wrappers; perhaps the the changes will be more concise.

@nschloe nschloe marked this pull request as ready for review April 15, 2021 09:53
@nschloe
Copy link
Contributor Author

nschloe commented Apr 15, 2021

I think the PR is in better shape now. I essentially moved the scalar conversion to into ScalarFunction, and by that could save a couple of checks here and there.

@andyfaff
Copy link
Contributor

I've not had much time to spend on scipy over the last few months, and don't think I'll be able to look before the branch point.

@peterbell10
Copy link
Member

I don't think my concern has been addressed: If NumPy is redefining what is acceptable to treat as a scalar, shouldn't it be up to the user-provided functions to conform to that definition?

A good argument against would be if scipy.optimize documents support for treating rank-1 arrays as scalars. In that case I would prefer a deprecation matching NumPy.

@nschloe
Copy link
Contributor Author

nschloe commented May 25, 2021

@peterbell10 I agree, but from what I recall scipy is rather conservative when it comes to making changes to the user interface. That's why I "fixed" some scipy functions to only hand shape-0 arrays to numpy. To illustrate, an example would be goal functionals like

def f(x):
     return np.array([x ** 2 - 1.0])

This works in scipy without warning now. It would have to be changed to

def f(x):
     return x ** 2 - 1.0

to avoid the warning. This PR makes sure both versions pass without warning.

@peterbell10
Copy link
Member

Yes, I know. I would like to see evidence that array-scalar conversion is a documented part of the current API, not just a coincidence due to NumPy's behavior. If it's not documented, then I see no reason to continue supporting it with an ugly workaround.

@nschloe
Copy link
Contributor Author

nschloe commented May 25, 2021

Just to be sure I understand correctly: You'd like to know if SciPy actually documents that you can do

def f(x):
     return np.array([x ** 2 - 1.0])

and if it's not documented, it'd be okay to just emit a warning like numpy does?

@nschloe
Copy link
Contributor Author

nschloe commented May 25, 2021

Perhaps an argument in favor of suppressing the warning:
Currently (and unfortunately), the input points x to an optimization method are always converted to shape (1,) if they are scalar. (See this issue report of mine which has been met with disagreement from scipy devs.) This means that in simple codes like

from scipy import optimize
import numpy as np


def f(x):
    out = -np.exp(-(x - 0.7)**2)
    return out

result = optimize.minimize(f, x0=0.3)

x and out will have shape (1,). (result.x is also of shape (1,) which I think is a mistake, but anyway.) With the change in numpy, the above code will print a warning. To avoid, the user would have to add [0]

from scipy import optimize
import numpy as np


def f(x):
    out = -np.exp(-(x - 0.7)**2)
    return out[0]

result = optimize.minimize(f, x0=0.3)

Not sure if that's desirable.

@tylerjereddy tylerjereddy modified the milestones: 1.7.0, 1.8.0 May 26, 2021
Copy link
Member

@peterbell10 peterbell10 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, although the minimize documentation doesn't explicitly support non-scalars. It does work for all methods currently, and consolidating handling into fewer places would make any future deprecation easier if we go down that route.

@nschloe
Copy link
Contributor Author

nschloe commented May 27, 2021

and consolidating handling into fewer places would make any future deprecation easier if we go down that route.

Indeed, that's what this PR is does, too (-> _wrap_scalar_function and ScalarFunction).

Copy link
Member

@peterbell10 peterbell10 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. I'll leave this open for a bit in case an optimize expert wants to comment.

@nschloe
Copy link
Contributor Author

nschloe commented Jun 21, 2021

I'll leave this open for a bit in case an optimize expert wants to comment.

@peterbell10 Perhaps now that 1.7.0 is released is a good time to merge?

@eric-wieser
Copy link
Contributor

eric-wieser commented Aug 17, 2021

Are you sure np.isscalar(x) is the right choice in this PR (vs np.ndim(x) == 0(? Its documentation indicates it is often the wrong choice.

@nschloe
Copy link
Contributor Author

nschloe commented Aug 17, 2021

@eric-wieser Good point. I've checked the use of isscalar in scipy and found that in almost all cases, one should use ndim == 0. I've opened a discussion here.

@peterbell10
Copy link
Member

np.ndim would be terrible here because it converts scalars into an array internally. So it's much slower in the case we want to be fast. I also don't see why we would care about object scalars here; everything should be numeric. So what are the downsides to np.isscalar then?

@nschloe
Copy link
Contributor Author

nschloe commented Aug 17, 2021

So what are the downsides to np.isscalar then?
everything should be numeric

Many of the isscalar conditionals are taking user input (e.g., from goal functionals in optimization), so we have no control over whether something is a Python scalar or not. The downside of isscalar is that "object scalars" (np.array(3.14)) are not isscalars. That's although they behave (almost) exactly like them.

So it's much slower in the case we want to be fast

It's faster indeed:

In [4]: %timeit np.ndim(3.14)
819 ns ± 0.784 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
In [5]: %timeit np.isscalar(3.14)
174 ns ± 0.705 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

We're talking about saving a few hundred nanoseconds here though which is less than the product of an integer with an object scalar:

In [6]: a = np.array(1.0)
In [7]: %timeit 2 * a
1.1 µs ± 8.02 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

A stronger argument for np.isscalar imho is readability. np.isscalar(x) is clear, np.dim(x) == 0 can be quite confusing. This leads me to believe that the better fix might be in numpy, not client code.

@peterbell10
Copy link
Member

By "object scalar" I mean re.compile or Exception which are the examples in numpy/numpy#11882. I'm fine with zero-d arrays not passing isscalar because they literally are not scalars...

@nschloe
Copy link
Contributor Author

nschloe commented Aug 17, 2021

I'm fine with zero-d arrays not passing isscalar because they literally are not scalars...

Hehe. – I would argue they are scalars because they small, taste, and look like scalars. You can't tell the difference. In numerical computation, they behave exactly the same. How the data is stored in memory is just an implementational detail.

Edit: The only difference I can think of right now are Python-list computations which fail for Python floats, 3.14 * [1.0] but succeed for np-scalars np.asarray(3.14) * [1.0] because the list is converted to an array first.

@peterbell10
Copy link
Member

peterbell10 commented Aug 17, 2021

Scalars are immutable and 0d arrays are mutable. That's a pretty big difference.

@peterbell10
Copy link
Member

At least this is the case for python int, float and NumPy scalars like np.float32.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
maintenance Items related to regular maintenance tasks
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants