Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] fix some dimensionality inconsistencies #11147

Closed
wants to merge 23 commits into from

Conversation

nschloe
Copy link
Contributor

@nschloe nschloe commented Nov 29, 2019

When working on numpy/numpy#10615, I noticed that scipy is in part inconsistent with whether it requires floats or arrays which may only contain one element. The work in numpy had me discover some bugs, e.g., arrays of obj type which look like

np.array([1.0, 4.0, np.array([2.5])])

which really wants to be float array

np.array([1.0, 4.0, 2.5])

I've fixed a bunch of those here.

Comments requested.

scipy/io/_fortran.py Outdated Show resolved Hide resolved
scipy/optimize/_basinhopping.py Outdated Show resolved Hide resolved
scipy/optimize/_basinhopping.py Outdated Show resolved Hide resolved
scipy/integrate/tests/test_bvp.py Outdated Show resolved Hide resolved
scipy/optimize/_shgo.py Outdated Show resolved Hide resolved
scipy/optimize/tests/test__numdiff.py Outdated Show resolved Hide resolved
@@ -185,6 +185,8 @@ def fun_non_numpy(self, x):
return math.exp(x)

def jac_non_numpy(self, x):
# Input can be float x or numpy.array([x]). Cast it all into numpy.array(x).
Copy link
Contributor

Choose a reason for hiding this comment

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

Again, this feels like a bug in the caller - it really ought to be certain what shape it calls this with.

Copy link
Contributor Author

@nschloe nschloe Nov 30, 2019

Choose a reason for hiding this comment

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

This is from x0 in scipy/optimize/_numdiff.py. The doc says

    x0 : array_like of shape (n,) or float
        Point at which to estimate the derivatives. Float will be converted
        to a 1-d array.

The Float will be converted to a 1-d array. is weird.

Copy link
Contributor

Choose a reason for hiding this comment

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

Honestly, it feels like what should actually happen is something like:

x0_flat = np.asarray(x0).ravel()

x0_revised = do_linalgy_stuff_that_needs_x_1d(x0_flat)

f(x0_revised.reshape(x0.shape))

Copy link
Contributor

Choose a reason for hiding this comment

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

I'll perhaps try a patch for that in the next few weeks

Copy link
Contributor

Choose a reason for hiding this comment

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

The purpose behind that docstring is to let the user know that f is going to be passed an array regardless of whether x is an array or a float.

@nschloe nschloe changed the title fix some dimensionality inconsistencies [WIP] fix some dimensionality inconsistencies Nov 30, 2019
@andyfaff
Copy link
Contributor

andyfaff commented Dec 1, 2019

Hmm, this PR is touching a lot of files and it's currently not clear to me that all the changes are required. Is back compatibility being conserved for all the changes (assuming there are no bugs), have tests been added to that effect?

Because it touches so many files I don't think it's going to be possible to give a deep enough review of all the changes. Can you give a more graded approach, with each specific set of code files being addressed one by one, and with necessary tests added. In this situation it's better to write the tests first so a real issue is demonstrated, then with a fix applied.

Let's take the differential_evolution code as an example. For the changes you made I'd like to see a test that has the objective function either returning a float, or an array with shape (1,). Only if the test shows an issue would it be time to do something.

>>> from scipy.optimize import differential_evolution, rosen
>>> def wr(x):
...     return np.array([rosen(x)])

>>> differential_evolution(rosen, [(0, 2), (0, 1)])
     fun: 0.0
 message: 'Optimization terminated successfully.'
    nfev: 4863
     nit: 161
 success: True
       x: array([1., 1.])
>>> differential_evolution(wr, [(0, 2), (0, 1)])
     fun: 0.0
 message: 'Optimization terminated successfully.'
    nfev: 4653
     nit: 154
 success: True
       x: array([1., 1.])

Here, there don't seem to be any issues, and it's not worth the code churn.

@eric-wieser
Copy link
Contributor

I would argue it is a bug that the second output does not say fun: [0.0]

scipy/optimize/_shgo.py Outdated Show resolved Hide resolved
@@ -56,7 +56,8 @@ class StructTest2(StructTestFunction):
"""

def f(self, x):
return (x - 30) * numpy.sin(x)
# f must return a scalar
return (x[0] - 30) * numpy.sin(x[0])
Copy link
Contributor

Choose a reason for hiding this comment

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

There's no point in making these kinds of changes. That is unless there is a test showing that the minimisation goes wrong with before vs after.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

One cannot optimize functions that return vectors. The change makes the expression mathematically sound and relieves numpy of the burden of having to convert your vector into a scalar in the background (which is what's happening now).

@pv
Copy link
Member

pv commented Dec 1, 2019 via email

@eric-wieser
Copy link
Contributor

eric-wieser commented Dec 1, 2019

I would argue it is a bug that the second output does not say fun: [0.0]

Actually, I've changed my mind here - optimize doesn't make sense on non-scalar results, given it's documented to "[minimize a] scalar function of one or more variables", and a 1-vector is not a scalar.

So the result of the second case is ok, but should be accompanied by a warning that the result is not a scalar.

@nschloe
Copy link
Contributor Author

nschloe commented Dec 1, 2019

Actually, I've changed my mind here - optimize doesn't make sense on non-scalar results, given it's documented to "[minimize a] scalar function of one or more variables", and a 1-vector is not a scalar.

👍

They are probably representative of the user code that exists out there, and we'd like to keep that working.

It makes no sense to provide a vector-returning function at an optimization method. Optimization can only happen on scalars. If your function returns a vector, then probably something is wrong. It's a good thing to have a warning about that.

@pv
Copy link
Member

pv commented Dec 1, 2019

It makes no sense to provide a vector-returning function at an optimization method

So far in numpy, size-1 arrays have been duck-scalars by definition in many contexts. Whether it makes sense or not does not matter in this context --- this has been how it has worked for 15 years, and we want to be sure the changes in this PR are not accidentally breaking the API compatibility.
This is why I would suggest to keep the tests as they are, and we can silence warnings as necessary, for the first pass of edits.

@nschloe
Copy link
Contributor Author

nschloe commented Dec 1, 2019

Okay, let me break up some of the smaller changes.

@mattip
Copy link
Contributor

mattip commented Dec 9, 2019

Some of these fixes are needed for the upcoming change in numpy that will warn when creating an object array from np.array([0.25, np.array([0.3])]). That change was reverted for now, but will be reinstated.

@nschloe
Copy link
Contributor Author

nschloe commented Jun 22, 2021

Closing in favor of #13864, #14274, and #14278.

@nschloe nschloe closed this Jun 22, 2021
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.

None yet

6 participants