Repository: scipy/scipy
Pull request: MAINT, DOC: optimize: Make the input validation explanation clear for `optimize.leastsq` and add a test.
<!-- 
Thanks for contributing a pull request! Please ensure that
your PR satisfies the checklist before submitting:
http://scipy.github.io/devdocs/dev/contributor/development_workflow.html#checklist-before-submitting-a-pr

Also, please name and describe your PR as you would write a
commit message:
https://docs.scipy.org/doc/numpy/dev/development_workflow.html#writing-the-commit-message

Note that we are a team of volunteers; we appreciate your
patience during the review process.

Again, thanks for contributing!
-->

#### Reference issue
<!--Example: Closes gh-WXYZ.-->
Ref: #13969

#### What does this implement/fix?
<!--Please explain your changes.-->
`optimize.leastsq` validates input vector and output vector length here:
https://github.com/scipy/scipy/blob/4ec4ab8d6ccc1cdb34b84fdcb66fde2cc0210dbf/scipy/optimize/minpack.py#L413-L415
But, the error msg does not explain what is `N` and `M`.

`optimize.leastsq` docstring explains meanings of N and M here:
https://github.com/scipy/scipy/blob/4ec4ab8d6ccc1cdb34b84fdcb66fde2cc0210dbf/scipy/optimize/minpack.py#L293-L295
But, it does not explain the input validation.

So, I improved the error msg and the docstring to explain each other.
Also, a test for this validation is missing, so I added it.


Problem statement:
N=4 must not exceed M=3
<!-- 
Thank you for taking the time to file a bug report. 
Please fill in the fields below, deleting the sections that 
don't apply to your issue. You can view the final output
by clicking the preview button above.
Note: This is a comment, and won't appear in the output.
-->

My issue is about ... from scipy.optimize import curve_fit

#### Reproducing code example:
<!-- 
If you place your code between the triple backticks below, 
it will be rendered as a code block. 
-->


<!-- If any, paste the *full* error message inside a code block
as above (starting from line Traceback)
-->

```
x = [0,75,745.5027446]
x = np.array(x).astype(float)
num = [1e-6,5,135.4783469]
y = np.array(num).astype(float)
param_bounds=([0,0,0,0],[np.inf,np.inf,np.inf,np.inf])
popt, pcov = curve_fit(func, x, y, bounds=param_bounds)
  ...
```

#### Scipy/Numpy/Python version information:
```
1.5.2 sys.version_info(major=3, minor=7, micro=9, releaselevel='final', serial=0)

```
-->




In my opinion, if the curving lack enough data, it be better says: I need 4 points for 3 paras, not 'N=4 must not exceed M=3'

Hints from issue discussion:
The code is here: 
https://github.com/scipy/scipy/blob/4ec4ab8d6ccc1cdb34b84fdcb66fde2cc0210dbf/scipy/optimize/minpack.py#L413-L415

`optimize.leastsq` docstring has meanings of `N` and `M` here:
https://github.com/scipy/scipy/blob/4ec4ab8d6ccc1cdb34b84fdcb66fde2cc0210dbf/scipy/optimize/minpack.py#L293-L295


I agree the error message can be improved.
I think this will be meaningful

if n > m: 
     raise TypeError('Improper input: The number of the initial parameters=%s must not exceed the number of model parameters needed=%s' % (n, m)) 
`m` is not "the number of model parameters needed". It's the number of data points (in most contexts where `leastsq()` is used).

Commit message: MAINT, TET: optimize: Make the input validation explanation clear for `optimize.leastsq` and add a test.

Patch to review:
diff --git a/scipy/optimize/minpack.py b/scipy/optimize/minpack.py
index 668ea4a1d93e..94b859ed0205 100644
--- a/scipy/optimize/minpack.py
+++ b/scipy/optimize/minpack.py
@@ -292,7 +292,7 @@ def leastsq(func, x0, args=(), Dfun=None, full_output=0,
     func : callable
         Should take at least one (possibly length N vector) argument and
         returns M floating point numbers. It must not return NaNs or
-        fitting might fail.
+        fitting might fail. M must be greater than or equal to N (M >= N).
     x0 : ndarray
         The starting estimate for the minimization.
     args : tuple, optional
@@ -411,7 +411,8 @@ def leastsq(func, x0, args=(), Dfun=None, full_output=0,
     m = shape[0]
 
     if n > m:
-        raise TypeError('Improper input: N=%s must not exceed M=%s' % (n, m))
+        raise TypeError('Improper input: func input vector length N=%s must '
+                        'not exceed func output vector length M=%s' % (n, m))
 
     if epsfcn is None:
         epsfcn = finfo(dtype).eps
diff --git a/scipy/optimize/tests/test_minpack.py b/scipy/optimize/tests/test_minpack.py
index d0a40f85be38..0a710ec5c38e 100644
--- a/scipy/optimize/tests/test_minpack.py
+++ b/scipy/optimize/tests/test_minpack.py
@@ -390,6 +390,15 @@ def test_concurrent_no_gradient(self):
     def test_concurrent_with_gradient(self):
         return sequence_parallel([self.test_basic_with_gradient] * 10)
 
+    def test_func_input_output_length_check(self):
+
+        def func(x):
+            return 2 * (x[0] - 3) ** 2 + 1
+
+        with assert_raises(TypeError,
+                           match='Improper input: func input vector length N='):
+            optimize.leastsq(func, x0=[0, 1])
+
 
 class TestCurveFit:
     def setup_method(self):


Code review comments:
diff_hunk: @@ -292,7 +292,7 @@ def leastsq(func, x0, args=(), Dfun=None, full_output=0,
     func : callable
         Should take at least one (possibly length N vector) argument and
         returns M floating point numbers. It must not return NaNs or
-        fitting might fail.
+        fitting might fail. M must be greater than or equal to N (M >= N).
path: scipy/optimize/minpack.py
text: @user1:
Don't need the "equation" for this simple text.
```suggestion
        fitting might fail. ``M`` must be greater than or equal to ``N``.
```

diff_hunk: @@ -292,7 +292,7 @@ def leastsq(func, x0, args=(), Dfun=None, full_output=0,
     func : callable
         Should take at least one (possibly length N vector) argument and
         returns M floating point numbers. It must not return NaNs or
path: scipy/optimize/minpack.py
text: @user1:
```suggestion
        Should take at least one (possibly length ``N`` vector) argument and
        returns ``M`` floating point numbers. It must not return NaNs or
```

diff_hunk: @@ -411,7 +411,8 @@ def leastsq(func, x0, args=(), Dfun=None, full_output=0,
     m = shape[0]
 
     if n > m:
-        raise TypeError('Improper input: N=%s must not exceed M=%s' % (n, m))
+        raise TypeError('Improper input: func input vector length N=%s must '
+                        'not exceed func output vector length M=%s' % (n, m))
path: scipy/optimize/minpack.py
text: @user1:
Use f-strings and double quotes as much as possible. Also now we ask to put the space on the new line.
```suggestion
        raise TypeError(f"Improper input: func input vector length N={n} must"
                        f" not exceed func output vector length M={m}")
```