Repository: scipy/scipy

Issue:
linalg.solve ill-condition warning
For the `linalg.solve`, since version 19.1, we have the capability of printing the warning the ill conditioning of the matrix. However the result is not quite pretty due to the traceback inclusion. For example: 

![image](https://user-images.githubusercontent.com/1303842/31029627-c0784a14-a552-11e7-8787-c4484766dbd8.png)

I think the story got a bit uglier with our latest change that also prints the reported dlamch machine precision since now we actually have to look for the correct number. 

![image](https://user-images.githubusercontent.com/1303842/31029713-1a406e46-a553-11e7-893b-ee26ceefda35.png)

I think it's best if we print the truncated versions of those numbers anyhow. But should we switch to a warning printing scheme instead? I don't know yet any way to raise a warning without printing the traceback. 

One use case I'd like to retain is that if a user is solving iteratively a lot of equations s/he should still be able to catch a warning if the problem is ill-conditioned (which was the reason to emit a warning in the first place). Or we can emit a warning directly and then do a custom printing. Such that there is a clear distinction between the traceback and the actual warning message. 




Discussion:
Can we set the `stacklevel` so that it's in the calling function? Then the traceback is much more useful.
In general, all uses of `warnings.warn` should have stacklevel set.
There's a xfailing test in scipy._lib checking for this.
I don't know actually. If it is too deep, let's say (I'm just making it up) something is computing a filter coefficient for conversion from state space to transfer function, to get the frequency response to draw Bode plot, which function should be the source; the deepest function or the calling function? 

Just to make an analogy to the BadFilterCoefficient warning case. 
>  If it is too deep,

This is a problem for the caller, whether it is a user or a library. In other words, in your example, it's a problem for the state space -> transfer function code; and we should improve our filter code so that it does the right thing with this warning (avoids it or catches it). So I'd say just go up however many stacklevels (2?) to get to the one that called e.g. `scipy.linalg.solve`.
I've played around with this a bit more and it turns out that setting the right value to the `stacklevel` is trickier. I've also [asked in SO](https://stackoverflow.com/q/46555075/4950339) to see whether someone teaches me more about it. Essentially there are two problems : 
```
from warnings import warn, formatwarning


def _warning_on_one_line(message, category, filename, lineno,
                         file=None, line=None):
    return ' %s:%s: %s:%s' % (filename, lineno, category.__name__, message)


def inner():
    formatwarning = _warning_on_one_line
    warn('Some warning', RuntimeWarning, stacklevel=3)
    return


def middle():
    return inner()


def outer():
    return middle()


outer()
```

As you play with `stacklevel` argument you can basically cause the warning to report different lines in this code. But if you overdo it you start getting IPython's caller stack all the way up to 26 where it finally starts to say `sys.1: RuntimeWarning` so detecting the correct caller doesn't seem obvious to me. And I agree that I think we have to be a bit more systematic when including a potentially warning emitting code inside a function. Such that we can catch the warning and emit a more meaningful message. So it seems to me that we are stuck with `stacklevel=1` for now. 

Second problem is how to format the warning message but that is kind how it is done as given in the above code.

Do you have a way to overcome this issue? 
My idea is/was:

> So I'd say just go up however many stacklevels (2?) to get to the one that called e.g. scipy.linalg.solve.

If we know where the warning is emitted, and we know the codepath to get there from `scipy.linalg.solve`, we can do this, right?
... in other words, in your example, if `scipy.linalg.solve` (in our codebase) is `inner`, you'd use `stacklevel=1`, if it's `middle` you'd use `stacklevel=2`, and if it's `outer` you'd use `stacklevel=3` (maybe +/-1 to all of these, can't remember the level details).
> So it seems to me that we are stuck with stacklevel=1 for now.

If you're saying that the warning is currently emitted in `scipy.linalg.solve`, then yeah I agree `stacklevel=1` (assuming that's the one that shows `scipy.linalg.solve`'s caller) is the right thing to do.
The stacktrace indicates the point in the *calling* code where the 
warning is emitted. The stacklevel must correspond to the innermost 
public API function. This is what Numpy does, and it is sensible behavior.

For a public API function, we write
```
def public_api_func():
     warnings.warn("some warning", stacklevel=2)
```
For an internal function for which we know the distance to the API function:
```
def _helper_func():
     warnings.warn("some warning", stacklevel=3)

def public_api_func():
     _helper_func()
```

> If we know where the warning is emitted, and we know the codepath to get there from scipy.linalg.solve, we can do this, right?

Yes indeed but depending on platform we might go too far and start addressing irrelevant parts of the current stack, that's what I meant by the 26 argument. For example if I use stacklevel=9

```
runfile('C:/Users/Ilhan Polat/Documents/pydump/warning_exp.py', wdir='C:/Users/Ilhan Polat/Documents/pydump')
c:\users\ilhan polat\appdata\local\programs\python\python36\lib\site-packages\IPython\core\interactiveshell.py:2808: RuntimeWarning: Some warning
  if self.run_code(code, result):
```

which is related to IPython internals So _give me the top of the stack_ kind of argument won't be accurate to get the actual code path. 


>The stacktrace indicates the point in the *calling* code where the warning is emitted. The stacklevel must correspond to the innermost public API function. This is what Numpy does, and it is sensible behavior. 

Ah OK now I get your arguments.
Warnings emitted inside Scipy code (e.g. due to calling solve() on a 
singular matrix in the implementation of some routine) are probably 
implementation defect on our side, although usually probably minor one.

Either we e.g. know the algorithm works correctly despite such 
singularity (in which case we should ensure somehow the warning doesn't 
get emitted), or the singularity implies a failure of the algorithm (in 
which case an error or a more specific warning would be more appropriate 
in the perfect world).

I agree but now I'm a bit confused how to detect an ill-conditioned array. Because if we remove the warning, then it goes unnoticed. This is known to matlab users a lot with its silent exception handling and only blowing up when the algorithm cannot move any forward. If we emit the warning only pointing to where linalg.solve is called then we have the current situation where the warning seems to be unrelated to the current caller.

Silencing the warning if it is not related is easy. Modifying the warning message is also doable by catching the warning. However I don't know what should be the way to go with this. 
> Because if we remove the warning, then it goes unnoticed

IIUC the propsal is to:

1. Always emit the warning in the `scipy.linalg.solve` code when ill conditioned matrices are detected.
2. In places in SciPy where we internally use `scipy.linalg.solve`, we should "do the right thing". In other words, we should deal with the warning safely so that no warning is ever emitted. This could mean "catch the warning and raise an error" in cases where rank deficiency is bad, or it could mean "catch the warning, and then use linalg.lstsq instead, because rank-deficiency is fine but we'd like the speed of solve if possible".

Point (2) is something we need to decide / do on a case-by-case basis as it comes up, so I don't think we need to worry about it right now.
OK, I'll just try to make the warning message a bit more structured then and leave the decision part for later then.

Resolving patch:
diff --git a/scipy/linalg/__init__.py b/scipy/linalg/__init__.py
--- a/scipy/linalg/__init__.py
+++ b/scipy/linalg/__init__.py
@@ -41,6 +41,7 @@
    matrix_balance - Balance matrix entries with a similarity transformation
    subspace_angles - Compute the subspace angles between two matrices
    LinAlgError
+   LinAlgWarning
 
 Eigenvalue Problems
 ===================
diff --git a/scipy/linalg/basic.py b/scipy/linalg/basic.py
--- a/scipy/linalg/basic.py
+++ b/scipy/linalg/basic.py
@@ -6,12 +6,12 @@
 
 from __future__ import division, print_function, absolute_import
 
-import warnings
+from warnings import warn
 import numpy as np
 from numpy import atleast_1d, atleast_2d
 from .flinalg import get_flinalg_funcs
 from .lapack import get_lapack_funcs, _compute_lwork
-from .misc import LinAlgError, _datacopied
+from .misc import LinAlgError, _datacopied, LinAlgWarning
 from .decomp import _asarray_validated
 from . import decomp, decomp_svd
 from ._solve_toeplitz import levinson
@@ -34,10 +34,9 @@ def _solve_check(n, info, lamch=None, rcond=None):
         return
     E = lamch('E')
     if rcond < E:
-        warnings.warn('scipy.linalg.solve\nIll-conditioned matrix detected.'
-                      ' Result is not guaranteed to be accurate.\nReciprocal '
-                      'condition number/precision: {} / {}'.format(rcond, E),
-                      RuntimeWarning)
+        warn('scipy.linalg.solve\nIll-conditioned matrix detected. Result '
+             'is not guaranteed to be accurate.\nReciprocal condition '
+             'number{:.6e}'.format(rcond), LinAlgWarning, stacklevel=3)
 
 
 def solve(a, b, sym_pos=False, lower=False, overwrite_a=False,
@@ -105,7 +104,7 @@ def solve(a, b, sym_pos=False, lower=False, overwrite_a=False,
         If size mismatches detected or input a is not square.
     LinAlgError
         If the matrix is singular.
-    RuntimeWarning
+    LinAlgWarning
         If an ill-conditioned input a is detected.
     NotImplementedError
         If transposed is True and input a is a complex matrix.
@@ -175,9 +174,9 @@ def solve(a, b, sym_pos=False, lower=False, overwrite_a=False,
 
     # Deprecate keyword "debug"
     if debug is not None:
-        warnings.warn('Use of the "debug" keyword is deprecated '
-                      'and this keyword will be removed in future '
-                      'versions of SciPy.', DeprecationWarning)
+        warn('Use of the "debug" keyword is deprecated '
+             'and this keyword will be removed in future '
+             'versions of SciPy.', DeprecationWarning, stacklevel=2)
 
     # Get the correct lamch function.
     # The LAMCH functions only exists for S and D
@@ -221,8 +220,8 @@ def solve(a, b, sym_pos=False, lower=False, overwrite_a=False,
         rcond, info = gecon(lu, anorm, norm=norm)
     # Hermitian case 'hesv'
     elif assume_a == 'her':
-        hecon, hesv, hesv_lw = get_lapack_funcs(('hecon', 'hesv', 'hesv_lwork'),
-                                                (a1, b1))
+        hecon, hesv, hesv_lw = get_lapack_funcs(('hecon', 'hesv',
+                                                 'hesv_lwork'), (a1, b1))
         lwork = _compute_lwork(hesv_lw, n, lower)
         lu, ipvt, x, info = hesv(a1, b1, lwork=lwork,
                                  lower=lower,
@@ -232,8 +231,8 @@ def solve(a, b, sym_pos=False, lower=False, overwrite_a=False,
         rcond, info = hecon(lu, ipvt, anorm)
     # Symmetric case 'sysv'
     elif assume_a == 'sym':
-        sycon, sysv, sysv_lw = get_lapack_funcs(('sycon', 'sysv', 'sysv_lwork'),
-                                                (a1, b1))
+        sycon, sysv, sysv_lw = get_lapack_funcs(('sycon', 'sysv',
+                                                 'sysv_lwork'), (a1, b1))
         lwork = _compute_lwork(sysv_lw, n, lower)
         lu, ipvt, x, info = sysv(a1, b1, lwork=lwork,
                                  lower=lower,
@@ -329,9 +328,9 @@ def solve_triangular(a, b, trans=0, lower=False, unit_diagonal=False,
 
     # Deprecate keyword "debug"
     if debug is not None:
-        warnings.warn('Use of the "debug" keyword is deprecated '
-                      'and this keyword will be removed in the future '
-                      'versions of SciPy.', DeprecationWarning)
+        warn('Use of the "debug" keyword is deprecated '
+             'and this keyword will be removed in the future '
+             'versions of SciPy.', DeprecationWarning, stacklevel=2)
 
     a1 = _asarray_validated(a, check_finite=check_finite)
     b1 = _asarray_validated(b, check_finite=check_finite)
@@ -427,9 +426,9 @@ def solve_banded(l_and_u, ab, b, overwrite_ab=False, overwrite_b=False,
 
     # Deprecate keyword "debug"
     if debug is not None:
-        warnings.warn('Use of the "debug" keyword is deprecated '
-                      'and this keyword will be removed in the future '
-                      'versions of SciPy.', DeprecationWarning)
+        warn('Use of the "debug" keyword is deprecated '
+             'and this keyword will be removed in the future '
+             'versions of SciPy.', DeprecationWarning, stacklevel=2)
 
     a1 = _asarray_validated(ab, check_finite=check_finite, as_inexact=True)
     b1 = _asarray_validated(b, check_finite=check_finite, as_inexact=True)
@@ -948,7 +947,7 @@ def inv(a, overwrite_a=False, check_finite=True):
     if len(a1.shape) != 2 or a1.shape[0] != a1.shape[1]:
         raise ValueError('expected square matrix')
     overwrite_a = overwrite_a or _datacopied(a1, a)
-    #XXX: I found no advantage or disadvantage of using finv.
+    # XXX: I found no advantage or disadvantage of using finv.
 #     finv, = get_flinalg_funcs(('inv',),(a1,))
 #     if finv is not None:
 #         a_inv,info = finv(a1,overwrite_a=overwrite_a)
@@ -1223,7 +1222,7 @@ def lstsq(a, b, cond=None, overwrite_a=False, overwrite_b=False,
                         # restart with gelss
                         lstsq.default_lapack_driver = 'gelss'
                         mesg += "Falling back to 'gelss' driver."
-                        warnings.warn(mesg, RuntimeWarning)
+                        warn(mesg, RuntimeWarning, stacklevel=2)
                         return lstsq(a, b, cond, overwrite_a, overwrite_b,
                                      check_finite, lapack_driver='gelss')
 
@@ -1264,6 +1263,8 @@ def lstsq(a, b, cond=None, overwrite_a=False, overwrite_b=False,
             x1 = x[:n]
             x = x1
         return x, np.array([], x.dtype), rank, None
+
+
 lstsq.default_lapack_driver = 'gelsd'
 
 
diff --git a/scipy/linalg/misc.py b/scipy/linalg/misc.py
--- a/scipy/linalg/misc.py
+++ b/scipy/linalg/misc.py
@@ -5,7 +5,15 @@
 from .blas import get_blas_funcs
 from .lapack import get_lapack_funcs
 
-__all__ = ['LinAlgError', 'norm']
+__all__ = ['LinAlgError', 'LinAlgWarning', 'norm']
+
+
+class LinAlgWarning(RuntimeWarning):
+    """
+    The warning emitted when a linear algebra related operation is close
+    to fail conditions of the algorithm or loss of accuracy is expected.
+    """
+    pass
 
 
 def norm(a, ord=None, axis=None, keepdims=False):


Test patch:
diff --git a/scipy/linalg/tests/test_basic.py b/scipy/linalg/tests/test_basic.py
--- a/scipy/linalg/tests/test_basic.py
+++ b/scipy/linalg/tests/test_basic.py
@@ -24,7 +24,7 @@
 from scipy.linalg import (solve, inv, det, lstsq, pinv, pinv2, pinvh, norm,
                           solve_banded, solveh_banded, solve_triangular,
                           solve_circulant, circulant, LinAlgError, block_diag,
-                          matrix_balance)
+                          matrix_balance, LinAlgWarning)
 
 from scipy.linalg.basic import LstsqLapackError
 from scipy.linalg._testutils import assert_no_overwrite
@@ -713,7 +713,7 @@ def test_ill_condition_warning(self):
         b = np.ones(2)
         with warnings.catch_warnings():
             warnings.simplefilter('error')
-            assert_raises(RuntimeWarning, solve, a, b)
+            assert_raises(LinAlgWarning, solve, a, b)
 
     def test_empty_rhs(self):
         a = np.eye(2)