Repository: scipy/scipy

Issue:
linalg.matrix_balance gives wrong transformation matrix
The documentation for linalg.matrix_balance states that
B, T = matrix_balance(A, permute=True, scale=True, separate=False, overwrite_a=False)

should yield matrices B and T such that 
B=T^-1 A T

However,

On my machine, the following code fails

```
import scipy.linalg as linalg
import numpy as np

A = np.array([[-0.5,  0.,  0.,  0. ],
               [ 0., -1.,  0. ,  0. ],
               [ 1.,  0., -0.5,  0. ],
               [ 0.,  1.,  0., -1. ]])

B, T = linalg.matrix_balance(A)

print 'T^-1 * A * T - B = '
print linalg.solve(T, A).dot(T) - B
# This should be all zeros, but it is not.
```
The output of running this code is 

```
[[-0.5 -1.   0.   0. ]
 [ 1.  -0.5  0.   0. ]
 [ 0.   0.   0.5 -1. ]
 [ 0.   0.   1.   0.5]]
```

I think the issue lies in the permutation, not the scaling. Basically, I think the output matrix is defined as 
T = DP
where D is the diagonal scaling matrix, and P is the permutation matrix. I think to correct the issue, we should have
T = D*P^T.

This is illustrated in the following code:
```
A2 = np.array([[-3.,  0.,  1.,  0.],
               [-1., -1., -0.,  1.],
               [-3., -0., -0.,  0.],
               [-1., -0.,  1., -1.]])
print '------------------'
B2, T2 = linalg.matrix_balance(A2)
print 'T2^-1 * A2 * T2 - B2 = '
print linalg.solve(T2, A2).dot(T2) - B2
# This is non zero on my machine


B2, (d,p) = linalg.matrix_balance(A2, separate=True)

D = np.diag(d)
P = np.eye(4)[p]
T_fix = D.dot(P.T)
print 'T_fix^-1 * A2 * T_fix  - B2 ='
print linalg.solve(T_fix, A2).dot(T_fix) - B2
# This is zero on my machine

```







Discussion:
Thanks for reporting it. I will take a look as soon as I have time today. It is possible that just the documentation is misleading too or there is a mistake with the generation of the outer factor T from the permutation.
If there are examples that work without any problems that's an indication that LAPACK is performing chain permutations and just using indices are not sufficient. 

What I mean is if row 1 is swapped with row 3 and then row 2 is swapped row 3 (which is now row 1) you get a permutation that is not possible to correct with straightforward indexing. 

``` 
 a = np.array([2,0,1,3,4])
 a[a]
array([1, 2, 0, 3, 4])
a[a][a]
array([0, 1, 2, 3, 4])
```
some group theoretical explanation is possible but I thought I have tackled this issue. Apparently not :)


Resolving patch:
diff --git a/scipy/linalg/basic.py b/scipy/linalg/basic.py
--- a/scipy/linalg/basic.py
+++ b/scipy/linalg/basic.py
@@ -1383,7 +1383,7 @@ def pinvh(a, cond=None, rcond=None, lower=True, return_rank=False,
 def matrix_balance(A, permute=True, scale=True, separate=False,
                    overwrite_a=False):
     """
-    A wrapper around LAPACK's xGEBAL routine family for matrix balancing.
+    Compute a diagonal similarity transformation for row/column balancing.
 
     The balancing tries to equalize the row and column 1-norms by applying
     a similarity transformation such that the magnitude variation of the
@@ -1429,8 +1429,8 @@ def matrix_balance(A, permute=True, scale=True, separate=False,
         integer powers of 2 to avoid numerical truncation errors.
     scale, perm : (n,) ndarray
         If ``separate`` keyword is set to True then instead of the array
-        ``T`` above, the scaling and the permutation vector is given
-        separately without allocating the full array ``T``.
+        ``T`` above, the scaling and the permutation vectors are given
+        separately as a tuple without allocating the full array ``T``.
 
     .. versionadded:: 0.19.0
 
@@ -1447,6 +1447,9 @@ def matrix_balance(A, permute=True, scale=True, separate=False,
     there are corner cases where balancing can actually worsen the
     conditioning. See [3]_ for such examples.
 
+    The code is a wrapper around LAPACK's xGEBAL routine family for matrix
+    balancing.
+
     Examples
     --------
     >>> from scipy import linalg
@@ -1519,4 +1522,8 @@ def matrix_balance(A, permute=True, scale=True, separate=False,
     if separate:
         return B, (scaling, perm)
 
-    return B, np.diag(scaling)[perm, :]
+    # get the inverse permutation
+    iperm = np.empty_like(perm)
+    iperm[perm] = np.arange(n)
+
+    return B, np.diag(scaling)[iperm, :]


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
@@ -1585,15 +1585,31 @@ def test_permutation(self):
 
     def test_perm_and_scaling(self):
         # Matrix with its diagonal removed
-        A = np.array([[0., 0., 0., 0., 0.000002],
-                      [0., 0., 0., 0., 0.],
-                      [2., 2., 0., 0., 0.],
-                      [2., 2., 0., 0., 0.],
-                      [0., 0., 0.000002, 0., 0.]])
-
-        x, y = matrix_balance(A)
-        x, (s, p) = matrix_balance(A, separate=1)
-        assert_allclose(y, np.diag(s)[p, :])
+        cases = (  # Case 0
+                 np.array([[0., 0., 0., 0., 0.000002],
+                           [0., 0., 0., 0., 0.],
+                           [2., 2., 0., 0., 0.],
+                           [2., 2., 0., 0., 0.],
+                           [0., 0., 0.000002, 0., 0.]]),
+                 #  Case 1 user reported GH-7258
+                 np.array([[-0.5, 0., 0., 0.],
+                           [0., -1., 0., 0.],
+                           [1., 0., -0.5, 0.],
+                           [0., 1., 0., -1.]]),
+                 #  Case 2 user reported GH-7258
+                 np.array([[-3., 0., 1., 0.],
+                           [-1., -1., -0., 1.],
+                           [-3., -0., -0., 0.],
+                           [-1., -0., 1., -1.]])
+                 )
+
+        for A in cases:
+            x, y = matrix_balance(A)
+            x, (s, p) = matrix_balance(A, separate=1)
+            ip = np.empty_like(p)
+            ip[p] = np.arange(A.shape[0])
+            assert_allclose(y, np.diag(s)[ip, :])
+            assert_allclose(solve(y, A).dot(y), x)
 
 
 if __name__ == "__main__":