Repository: scipy/scipy

Issue:
solve_discrete_are in scipy.linalg does (sometimes) not solve correctly (Trac #1732)
_Original ticket http://projects.scipy.org/scipy/ticket/1732 on 2012-09-20 by trac user inde, assigned to unknown._

Hi,
this is for scipy 0.11.0rc2. It seemed to be a normal release on sourceforge, but there is no 0.11 here, so I am filing under devel.

So the problem I am observing when solving the discrete algebraic Riccati Equation for the linear quadratic regulator problem is that sometimes the solution returned by scipy.linalg.solve_discrete_are is not positive definite or even correct.

I am testing with 3x3 random matrices (code below). The attached npz file contains one particular case that did not solve for me.

```
def dare_zero_compare(self,A,B,Q,R,S):
    AT_S_A = s.dot(A.T,s.dot(S,A))
    AT_S_B = s.dot(A.T,s.dot(S,B))
    BT_S_B_plusR_inv = s.linalg.inv(s.dot(B.T,s.dot(S,B)) + R)
    BT_S_A = s.dot(B.T,s.dot(S,A))

    zeroS = AT_S_A - S - s.dot(AT_S_B, s.dot(BT_S_B_plusR_inv, BT_S_A)) + Q
    return zeroS

def test_dareSsolves(self):
    for i in range(100):
        n = 3
        A = s.random.rand(n,n)
        B = s.random.rand(n,1)
        R = s.eye(B.shape[1])
        Q = s.eye(A.shape[0])


        S = s.linalg.solve_discrete_are(A,B,Q,R)

        zeroS = self.dare_zero_compare(A,B,Q,R,S)            
        try:
            for ai in zeroS:
                for aij in ai:
                    self.assertAlmostEqual(aij, 0.0)
        except AssertionError:
            with open ('test_dareSsolves.m','w') as f:
                f.write(string_dare_test(A,B,Q,R,S) )
            s.savez('test_dareSsolves.npz', A=A,B=B,Q=Q,R=R,S=S)

def test_dareSsolves_repeat(self):
    npzfile = s.load('test_dareSsolves.npz')
    A = npzfile['A']
    B = npzfile['B']
    Q = npzfile['Q']
    R = npzfile['R']
    S = npzfile['S']

    S_new = s.linalg.solve_discrete_are(A,B,Q,R)
    print A,B, Q,R,S,S_new

    zeroS = self.dare_zero_compare(A,B,Q,R,S)
    zeroS_new = self.dare_zero_compare(A,B,Q,R,S_new)

    for ai in zeroS:
        for aij in ai:
            self.assertAlmostEqual(aij, 0.0)
            raise AssertionError
```



Discussion:
Attachment added by trac user inde on 2012-09-20: [test_dareSsolves.npz](http://projects.scipy.org/scipy/attachment/ticket/1732/test_dareSsolves.npz)

Attachment added by trac user inde on 2012-09-20: [dlqr_test.py](http://projects.scipy.org/scipy/attachment/ticket/1732/dlqr_test.py)

_trac user inde wrote on 2012-09-20_

code did unfortunately not come out right, so I added the unit test file as well.
I commented out the parts where I write code to a Matlab .m file for double-checking (the test cases do solve there) and where I test my dlqr implementation.

_trac user inde wrote on 2012-09-20_

dlqr_test.py needs import scipy.linalg to run.

_@rgommers wrote on 2012-09-21_

Fails for me too.

Jeff, what do you think?

_trac user ArmstrongJ wrote on 2012-09-22_

I'll look into the particular solution, but the technique used to arrive at the solution, Laub's Schur method, does not guarantee a valid solution in all cases.  Laub's technique is a simple, straightforward solution to the DARE.  However, a significant number of cases fail using the direct solver.  I talk a bit about it in my PyCon talk in 2011.  Alternate solvers, such as an iterative technique or SLICOT's implementation, might be better in some cases.

MATLAB uses SLICOT to solve the algebraic Riccati equations, which is more robust.  However, SLICOT is GPL, so here we are.  

Again, I'll have a look at the problematic case, but there is no guarantee of a solution using the technique included in SciPy. 

_@rgommers wrote on 2012-09-22_

How about doing one or more of the following:
- document that this method is less robust than iterative methods
- link to SLICOT and/or the best Python wrapper for it in the docstring
- Add an optional check (with default to do the check) which validates the answer to some decimals by plugging the answer back into the equation, and issue a warning if the answer isn't correct/accurate?

_trac user inde wrote on 2012-09-26_

Hi,
first of all. I am impressed that someone is looking at this so fast. I must say that I like scipy more and more.

I just wanted to add that I had similar / the same problems when I tried solving dare via Murray's control library and the slycot interface to SLICOT.

I am currently doing it via oct2py. It's working so far, but has other traps.

I've also encountered this.
Are there any plans to this issue? If not could you add a warning to the documentation (http://docs.scipy.org/doc/scipy-0.16.0/reference/generated/scipy.linalg.solve_discrete_are.html)?

Patches welcome I guess: https://github.com/scipy/scipy/issues/2251#issuecomment-17028292

> MATLAB uses SLICOT to solve the algebraic Riccati equations, which is more robust. However, SLICOT is GPL, so here we are.

Wait, how does this work? Does this mean MATLAB is free now? :)  Have they cleverly partitioned their software to work around the GPL? Should scipy do that too? Did they get a separate license? Could scipy get one?

Not sure how it works, but Matlab also uses fftw.

Both SLICOT and FFTW are available for commercial use if you buy the
appropriate license.

On Thu, Nov 19, 2015 at 9:28 AM, Charles Harris notifications@github.com
wrote:

> Not sure how it works, but Matlab also uses fftw.
> 
> —
> Reply to this email directly or view it on GitHub
> https://github.com/scipy/scipy/issues/2251#issuecomment-158072967.

Can someone point me to the problematic case as the attachment is not available anymore? 

I think we have an archive of the old Trac instance somewhere on a server, but that'll be hard to dig up. @pv migrated projects.scipy.org content to our Amazon server says my mail history. Can't get into that server now though, and don't have admin rights anyway.

The easiest for this issue may be to just generate random 3x3 matrices until one fails. Several people have hit this issue, so it shouldn't take too long.

You can get it via
`wget --header="Host: projects.scipy.org" http://scipy.org/scipy/attachment/ticket/1732/test_dareSsolves.npz`
The DNS is pointing to the wrong place.

@rgommers  The reason I'm asking is that there are strict theoretical assumptions on the data matrices such observability and other details and I was curious whether this is a similar issue with the data. 

@pv Thanks. Is there a way for me to get it on Win 10 machine? I get 404 for the address above. 

Python 3:

```
from urllib.request import Request, urlopen
req = Request("http://scipy.org/scipy/attachment/ticket/1732/test_dareSsolves.npz",
    headers={'Host': 'projects.scipy.org'})
with open('downloaded.npz', 'wb') as f:
    f.write(urlopen(req).read())
```

Thanks to @pv, this was the data that was attached 

```
A = np.array([[ 0.63399379,  0.54906824,  0.76253406],
       [ 0.5404729 ,  0.53745766,  0.08731853],
       [ 0.27524045,  0.84922129,  0.4681622 ]])
B = np.array([[ 0.96861695],[ 0.05532739],[ 0.78934047]])
Q = np.eye(3)
R = 1.
S = np.array([[-2.67522766, -5.39447418,  2.19128542],
       [-1.94918951, -3.15480639,  5.24379117],
       [ 4.29133973,  8.10585767, -5.88895897]])
```

It turns out that these problematic cases have all common fact that the ordering of the Schur algorithm is giving wrong answers. Or better put, it cannot reliably reorder and isolate the stable invariant subspace of the symplectic matrix. 

For this specific example, the Schur decomposition gives 

```

    0.5253    3.1176   -1.8481   -1.6505   -0.4582   -1.5179
   -3.0364    0.5253   -0.1033    2.1546   -1.5363    0.8592
         0         0    0.0539   -0.3675    0.2833   -1.2566
         0         0    0.2714    0.0539   -0.0023    0.4698
         0         0         0         0    0.4739   -0.5032
         0         0         0         0         0    2.1103
```

Notice that the 0.4739 should be on 3,3 diagonal and the first 2x2 block should be swapped with the second 2x2 block. I've checked a few more random examples and the result is always consistently wrong reordering. This is probably happening because A matrix is not stable and the reordering misses this possibility. 

Lack of symmetry is also used by matlab to check whether the algorithm failed to isolate the correct subspace. 

I've written more extensive pure Py3 Riccati and Lyapunov solvers very recently using extended pencils and hopefully I'll add Newton iteration on top (which is how I found this issue actually) to get more accuracy. But I don't know if they are suitable for SciPy. They can be seen [in harold repository](https://github.com/ilayn/harold/blob/master/harold/harold.py#L6009). It's MIT licensed so can be imported directly but again I'm not sure about my coding skills. At least I've tested the results extensively against CAREX and DAREX Riccati problem libraries. 

So, it can form the extended symplectic pencil to improve it. I can provide a version of it but I'm really not verse in pull request handling. So should you be so kind to point me a tutorial, I can at least quickly include that. That would also remove the unnecessary inversion of A matrix. 

@ilayn: https://github.com/scipy/scipy/blob/master/HACKING.rst.txt


Resolving patch:
diff --git a/scipy/linalg/_solvers.py b/scipy/linalg/_solvers.py
--- a/scipy/linalg/_solvers.py
+++ b/scipy/linalg/_solvers.py
@@ -1,19 +1,25 @@
 """Matrix equation solver routines"""
-
 # Author: Jeffrey Armstrong <jeff@approximatrix.com>
 # February 24, 2012
 
 # Modified: Chad Fulton <ChadFulton@gmail.com>
 # June 19, 2014
 
+# Modified: Ilhan Polat <ilhanpolat@gmail.com>
+# September 13, 2016
+
 from __future__ import division, print_function, absolute_import
 
 import numpy as np
-from numpy.linalg import inv, LinAlgError
+from numpy.linalg import inv, LinAlgError, norm, cond
 
-from .basic import solve
+from .basic import solve, solve_triangular
 from .lapack import get_lapack_funcs
 from .decomp_schur import schur
+from .decomp_lu import lu
+from .decomp_qr import qr
+from ._decomp_qz import ordqz
+from .decomp import _asarray_validated
 from .special_matrices import kron
 
 __all__ = ['solve_sylvester', 'solve_lyapunov', 'solve_discrete_lyapunov',
@@ -231,7 +237,7 @@ def solve_continuous_are(a, b, q, r):
     The CARE is defined as
 
     .. math::
-        (A'X + XA - XBR^-1B'X+Q=0)
+        A'X + XA - XB R^{-1} B'X + Q = 0
 
     It is solved directly using a Schur decomposition method.
 
@@ -257,14 +263,19 @@ def solve_continuous_are(a, b, q, r):
 
     Notes
     -----
-    Method taken from:
-    Laub, "A Schur Method for Solving Algebraic Riccati Equations."
-    U.S. Energy Research and Development Agency under contract
-    ERDA-E(49-18)-2087.
-    http://dspace.mit.edu/bitstream/handle/1721.1/1301/R-0859-05666488.pdf
+    Method is taken from [1]_.
 
     .. versionadded:: 0.11.0
 
+    
+    References
+    ----------
+    
+    .. [1] Alan J Laub, "A Schur Method for Solving Algebraic Riccati 
+       Equations.", Massachusetts Institute of Technology. Laboratory for 
+       Information and Decision Systems. LIDS-R ; 859. Available online :
+       http://hdl.handle.net/1721.1/1301
+        
     """
 
     try:
@@ -302,25 +313,38 @@ def solve_discrete_are(a, b, q, r):
     The DARE is defined as
 
     .. math::
-        X = A'XA-(A'XB)(R+B'XB)^-1(B'XA)+Q
+        
+        X = A'XA - (A'XB) (R+B'XB)^{-1} (B'XA) + Q
 
-    It is solved directly using a Schur decomposition method.
+    The limitations for a solution to exist are :
+        
+        * All eigenvalues of :math:`A` outside the unit disc, should be 
+          controllable. 
 
+        * The associated symplectic pencil (See Notes), should have 
+          eigenvalues sufficiently away from the unit circle. 
+    
     Parameters
     ----------
     a : (M, M) array_like
-        Non-singular, square matrix
+        Square matrix
     b : (M, N) array_like
         Input
     q : (M, M) array_like
         Input
     r : (N, N) array_like
-        Non-singular, square matrix
+        Square matrix
 
     Returns
     -------
     x : ndarray
-        Solution to the continuous Lyapunov equation
+        Solution to the discrete algebraic Riccati equation.
+        
+    Raises
+    ------
+    LinAlgError
+        For cases where the stable subspace of the pencil could not be 
+        isolated. See Notes section and the references for details. 
 
     See Also
     --------
@@ -328,45 +352,119 @@ def solve_discrete_are(a, b, q, r):
 
     Notes
     -----
-    Method taken from:
-    Laub, "A Schur Method for Solving Algebraic Riccati Equations."
-    U.S. Energy Research and Development Agency under contract
-    ERDA-E(49-18)-2087.
-    http://dspace.mit.edu/bitstream/handle/1721.1/1301/R-0859-05666488.pdf
+    The equation is solved by forming the extended symplectic matrix pencil, 
+    as described in [1]_, :math:`H - \lambda J` given by the block 
+    matrices::
+        
+    
+           [  A   0   B ]             [ I   0   B ] 
+           [ -Q   I   0 ] - \lambda * [ 0  A^T  0 ]
+           [  0   0   R ]             [ 0 -B^T  0 ]
+        
+    and using a QZ decomposition method.
+    
+    In this algorithm, the fail conditions are linked to the symmetrycity 
+    of the product :math:`U_2 U_1^{-1}` and condition number of 
+    :math:`U_1`. Here, :math:`U` is the 2m-by-m matrix that holds the 
+    eigenvectors spanning the stable subspace with 2m rows and partitioned 
+    into two m-row matrices. See [1]_ and [2]_ for more details.
+    
 
     .. versionadded:: 0.11.0
 
+    References
+    ----------
+    .. [1]  P. van Dooren , "A Generalized Eigenvalue Approach For Solving 
+       Riccati Equations.", SIAM Journal on Scientific and Statistical 
+       Computing, Vol.2(2), DOI: 10.1137/0902010
+       
+    .. [2] Alan J Laub, "A Schur Method for Solving Algebraic Riccati 
+       Equations.", Massachusetts Institute of Technology. Laboratory for 
+       Information and Decision Systems. LIDS-R ; 859. Available online :
+       http://hdl.handle.net/1721.1/1301
+       
     """
-
-    try:
-        g = inv(r)
-    except LinAlgError:
-        raise ValueError('Matrix R in the algebraic Riccati equation solver '
-                         'is ill-conditioned')
-
-    g = np.dot(np.dot(b, g), b.conj().transpose())
-
-    try:
-        ait = inv(a).conj().transpose()  # ait is "A inverse transpose"
-    except LinAlgError:
-        raise ValueError('Matrix A in the algebraic Riccati equation solver '
-                         'is ill-conditioned')
-
-    z11 = a+np.dot(np.dot(g, ait), q)
-    z12 = -1.0*np.dot(g, ait)
-    z21 = -1.0*np.dot(ait, q)
-    z22 = ait
-
-    z = np.vstack((np.hstack((z11, z12)), np.hstack((z21, z22))))
-
-    # Note: we need to sort the upper left of s to lie within the unit circle,
-    #       while the lower right is outside (Laub, p. 7)
-    s, u, _ = schur(z, sort='iuc')
-
-    (m, n) = u.shape
-
-    u11 = u[0:m//2, 0:n//2]
-    u21 = u[m//2:m, 0:n//2]
-    u11i = inv(u11)
-
-    return np.dot(u21, u11i)
+    
+    a = np.atleast_2d(_asarray_validated(a))
+    b = np.atleast_2d(_asarray_validated(b))
+    q = np.atleast_2d(_asarray_validated(q))
+    r = np.atleast_2d(_asarray_validated(r))
+    
+    # Get the correct data types otherwise Numpy complains about pushing 
+    # complex numbers into real arrays.
+    r_or_c = complex if np.iscomplexobj(b) else float
+
+    for ind, mat in enumerate((a,q,r)):
+        if np.iscomplexobj(mat):
+            r_or_c = complex
+            
+        if not np.equal(*mat.shape):
+            raise ValueError("Matrix {} should be square.".format("aqr"[ind]))
+    
+    # Shape consistency checks
+    m, n = b.shape
+    if m != a.shape[0]:
+        raise ValueError("Matrix a and b should have the same number of rows.")
+    if m != q.shape[0]:
+        raise ValueError("Matrix a and q should have the same shape.")
+    if n != r.shape[0]:
+        raise ValueError("Matrix b and r should have the same number of cols.")
+
+    # Check if the data matrices q, r are (sufficiently) hermitian
+    for ind, mat in enumerate((q,r)):
+        if norm(mat - mat.conj().T,1) > np.spacing(norm(mat,1))*100:
+            raise ValueError("Matrix {} should be symmetric/hermitian."
+                             "".format("qr"[ind]))
+
+    # Form the matrix pencil        
+    H = np.zeros((2*m + n, 2*m + n), dtype = r_or_c)
+    H[:m, :m] = a
+    H[:m, 2*m:] = b
+    H[m:2*m, :m] = -q
+    H[m:2*m, m:2*m] = np.eye(m)
+    H[2*m:, 2*m:] = r
+
+    J = np.zeros_like(H, dtype = r_or_c)
+    J[:m, :m] = np.eye(m)
+    J[m:2*m, m:2*m] = a.conj().T
+    J[2*m:, m:2*m] = -b.conj().T
+
+    # Deflate the pencil by the R column ala Ref. [1]
+    q_of_qr, _ = qr(H[:,-n:])
+    H = q_of_qr[:,n:].conj().T.dot(H[:,:2*m])
+    J = q_of_qr[:,n:].conj().T.dot(J[:,:2*m])
+    
+    # Decide on which output type is needed for QZ 
+    out_str = 'real' if r_or_c == float else 'complex'
+    
+    _, _, _, _, _, u = ordqz(H, J, sort = 'iuc', overwrite_a=True, 
+                    overwrite_b=True, check_finite=False, output=out_str)
+    
+    # Get the relevant parts of the stable subspace basis
+    u00 = u[:m, :m]
+    u10 = u[m:, :m]
+    
+    # Solve via back-substituion after checking the condition of u00 
+    up, ul, uu = lu(u00)
+
+    if 1/cond(uu) < np.spacing(1.):
+        raise LinAlgError('Failed to find a finite solution.')
+    
+    # Exploit the triangular structure
+    x = solve_triangular(ul.conj().T,
+                         solve_triangular(uu.conj().T, 
+                                          u10.conj().T,
+                                          lower=True),
+                         unit_diagonal=True,
+                         ).conj().T.dot(up.conj().T)
+
+    # Check the deviation from symmetry for success
+    u_sym = u00.conj().T.dot(u10)
+    u_sym = u_sym - u_sym.conj().T
+    sym_threshold = np.max(np.spacing([1000., norm(u_sym, 1)]))
+
+    if np.max(np.abs(u_sym)) > sym_threshold:
+        raise LinAlgError('The associated symplectic pencil has eigenvalues'
+                          'too close to the unit circle')
+    
+    return (x + x.conj().T)/2


Test patch:
diff --git a/scipy/linalg/tests/test_solvers.py b/scipy/linalg/tests/test_solvers.py
--- a/scipy/linalg/tests/test_solvers.py
+++ b/scipy/linalg/tests/test_solvers.py
@@ -1,12 +1,14 @@
 from __future__ import division, print_function, absolute_import
 
 import numpy as np
-from numpy.linalg import inv
+from numpy.linalg import inv, solve
 
 from numpy.testing import TestCase, rand, run_module_suite, assert_raises, \
     assert_equal, assert_almost_equal, assert_array_almost_equal, assert_, \
     assert_allclose
 
+from numpy.testing.noseclasses import KnownFailureTest
+    
 from scipy.linalg import solve_sylvester, solve_lyapunov, \
     solve_discrete_lyapunov, solve_continuous_are, solve_discrete_are
 
@@ -131,48 +133,214 @@ def test_cases(self):
         for case in self.cases:
             self.check_case(case[0], case[1], case[2], case[3])
 
-
-class TestSolveDiscreteARE(TestCase):
+def test_solve_discrete_are():
 
     cases = [
-        # Difficult from a numerical standpoint, again from Laub, A. J.
-        # (http://dspace.mit.edu/bitstream/handle/1721.1/1301/R-0859-05666488.pdf)
-        (np.matrix([[4, 3], [-9.0/2.0, -7.0/2.0]]),
-         np.matrix([[1,], [-1,]]),
-         np.matrix([[9, 6], [6, 4]]),
-         np.matrix([[1,],])),
-        # Another example from Laub
-        (np.matrix([[0.9512, 0], [0, 0.9048]]),
-         np.matrix([[4.877, 4.877], [-1.1895, 3.569]]),
-         np.matrix([[0.005, 0],[0, 0.02]]),
-         np.matrix([[1.0/3.0, 0],[0, 3]])),
+        # Darex examples taken from (with default parameters):
+        # [1] P.BENNER, A.J. LAUB, V. MEHRMANN: 'A Collection of Benchmark 
+        #     Examples for the Numerical Solution of Algebraic Riccati 
+        #     Equations II: Discrete-Time Case', Tech. Report SPC 95_23, 
+        #     Fak. f. Mathematik, TU Chemnitz-Zwickau (Germany), 1995.
+        # [2] T. GUDMUNDSSON, C. KENNEY, A.J. LAUB: 'Scaling of the 
+        #     Discrete-Time Algebraic Riccati Equation to Enhance Stability 
+        #     of the Schur Solution Method', IEEE Trans.Aut.Cont., vol.37(4) 
+        #
+        # The format of the data is (a, b, q, r, knownfailure), where
+        # knownfailure is None if the test passes or a string
+        # indicating the reason for failure.
+        #
         # Complex a; real b, q, r
-        (np.matrix([[2, 1-2j], [0, -3j]]),
-         np.matrix([[0,], [1,]]),
-         np.matrix([[1, 0], [0, 2]]),
-         np.matrix([[1,],])),
+        (np.array([[2, 1-2j], [0, -3j]]),
+         np.array([[0,], [1,]]),
+         np.array([[1, 0], [0, 2]]),
+         np.array([[1,],]),
+         None),
         # Real a, q, r; complex b
-        (np.matrix([[2, 1], [0, -1]]),
-         np.matrix([[-2j,], [1j,]]),
-         np.matrix([[1, 0], [0, 2]]),
-         np.matrix([[1,],])),
-        # Real a, b; complex q, r
-        (np.matrix([[3, 1], [0, -1]]),
-         np.matrix([[1, 2], [1, 3]]),
-         np.matrix([[1, -3j], [1-1j, 2]]),
-         np.matrix([[-2j, 2], [1j, 3]])),
+        (np.array([[2, 1], [0, -1]]),
+         np.array([[-2j,], [1j,]]),
+         np.array([[1, 0], [0, 2]]),
+         np.array([[1,],]),
+         None),
+        # Real a, b; complex q, r 
+        (np.array([[3, 1], [0, -1]]),
+         np.array([[1, 2], [1, 3]]),
+         np.array([[1, 1+1j], [1-1j, 2]]),
+         np.array([[2, -2j], [2j, 3]]),
+         None),
+        # User-reported gh-2251 (Trac #1732)
+        (np.array([[0.63399379, 0.54906824, 0.76253406],
+                   [0.5404729, 0.53745766, 0.08731853],
+                   [0.27524045, 0.84922129, 0.4681622]]),
+         np.array([[0.96861695],[0.05532739],[0.78934047]]),
+         np.eye(3),
+         np.eye(1),
+         None),
+        # darex #1
+        (np.array([[4, 3],[-4.5, -3.5]]),
+         np.array([[1],[-1]]),
+         np.array([[9, 6],[6, 4]]),
+         np.array([[1]]),
+         None),
+        # darex #2
+        (np.array([[0.9512, 0],[0, 0.9048]]),
+         np.array([[4.877, 4.877],[-1.1895, 3.569]]),
+         np.array([[0.005, 0],[0, 0.02]]),
+         np.array([[1/3, 0],[0, 3]]),
+         None),
+        # darex #3
+        (np.array([[2, -1],[1, 0]]),
+         np.array([[1],[0]]),
+         np.array([[0, 0],[0, 1]]),
+         np.array([[0]]),
+         None),
+        # darex #4 (skipped the gen. Ric. term S)
+        (np.array([[0, 1],[0, -1]]),
+         np.array([[1, 0],[2, 1]]),
+         np.array([[-4, -4],[-4, 7]]) * (1/11),
+         np.array([[9, 3],[3, 1]]),
+         None),
+        # darex #5
+        (np.array([[0, 1],[0, 0]]),
+         np.array([[0],[1]]),
+         np.array([[1, 2],[2, 4]]),
+         np.array([[1]]),
+         None),
+        # darex #6
+        (np.array([[0.998, 0.067, 0, 0],
+                   [-.067, 0.998, 0, 0],
+                   [0, 0, 0.998, 0.153],
+                   [0, 0, -.153, 0.998]]),
+         np.array([[0.0033, 0.0200],
+                   [0.1000, -.0007],
+                   [0.0400, 0.0073],
+                   [-.0028, 0.1000]]),
+         np.array([[1.87, 0, 0, -0.244],
+                   [0, 0.744, 0.205, 0],
+                   [0, 0.205, 0.589, 0],
+                   [-0.244, 0, 0, 1.048]]),
+         np.eye(2),
+         None),
+        # darex #7
+        (np.array([[0.984750, -.079903, 0.0009054, -.0010765],
+                   [0.041588, 0.998990, -.0358550, 0.0126840],
+                   [-.546620, 0.044916, -.3299100, 0.1931800],
+                   [2.662400, -.100450, -.9245500, -.2632500]]),
+         np.array([[0.0037112, 0.0007361],
+                   [-.0870510, 9.3411e-6],
+                   [-1.198440, -4.1378e-4],
+                   [-3.192700, 9.2535e-4]]),
+         np.eye(4)*1e-2,
+         np.eye(2),
+         None),
+        # darex #8
+        (np.array([[-0.6000000, -2.2000000, -3.6000000, -5.4000180],
+                   [1.0000000, 0.6000000, 0.8000000, 3.3999820],
+                   [0.0000000, 1.0000000, 1.8000000, 3.7999820],
+                   [0.0000000, 0.0000000, 0.0000000, -0.9999820]]),
+         np.array([[1.0, -1.0, -1.0, -1.0],
+                   [0.0, 1.0, -1.0, -1.0],
+                   [0.0, 0.0, 1.0, -1.0],
+                   [0.0, 0.0, 0.0, 1.0]]),
+         np.array([[2, 1, 3, 6],
+                   [1, 2, 2, 5],
+                   [3, 2, 6, 11],
+                   [6, 5, 11, 22]]),
+         np.eye(4),
+         None),
+        # darex #9
+        (np.array([[95.4070, 1.9643, 0.3597, 0.0673, 0.0190],
+                    [40.8490, 41.3170, 16.0840, 4.4679, 1.1971],
+                   [12.2170, 26.3260, 36.1490, 15.9300, 12.3830],
+                   [4.1118, 12.8580, 27.2090, 21.4420, 40.9760],
+                   [0.1305, 0.5808, 1.8750, 3.6162, 94.2800]]) * 0.01,
+         np.array([[0.0434, -0.0122],
+                   [2.6606, -1.0453],
+                   [3.7530, -5.5100],
+                   [3.6076, -6.6000],
+                   [0.4617, -0.9148]]) * 0.01,
+         np.eye(5),
+         np.eye(2),
+         None),
+        # darex #10
+        (np.kron(np.eye(2),np.diag([1,1],k=1)),
+         np.kron(np.eye(2),np.array([[0],[0],[1]])),
+         np.array([[1, 1, 0, 0, 0, 0],
+               [1, 1, 0, 0, 0, 0],
+               [0, 0, 0, 0, 0, 0],
+               [0, 0, 0, 1, -1, 0],
+               [0, 0, 0, -1, 1, 0],
+               [0, 0, 0, 0, 0, 0]]),
+         np.array([[3, 0],[0, 1]]),
+         None),
+        # darex #11
+        (0.001 * np.array(
+         [[870.1, 135.0, 11.59, .5014, -37.22, .3484, 0, 4.242, 7.249],
+         [76.55, 897.4, 12.72, 0.5504, -40.16, .3743, 0, 4.53, 7.499],
+         [-127.2, 357.5, 817, 1.455, -102.8, .987, 0, 11.85, 18.72],
+         [-363.5, 633.9, 74.91, 796.6, -273.5, 2.653, 0, 31.72, 48.82],
+         [-960, 1645.9, -128.9, -5.597, 71.42, 7.108, 0, 84.52, 125.9],
+         [-664.4, 112.96, -88.89, -3.854, 84.47, 13.6, 0, 144.3, 101.6],
+         [-410.2, 693, -54.71, -2.371, 66.49, 12.49, .1063, 99.97, 69.67],
+         [-179.9, 301.7, -23.93, -1.035, 60.59, 22.16, 0, 213.9, 35.54],
+         [-345.1, 580.4, -45.96, -1.989, 105.6, 19.86, 0, 219.1, 215.2]]),
+         np.array([[4.7600, -0.5701, -83.6800],
+               [0.8790, -4.7730, -2.7300],
+               [1.4820, -13.1200, 8.8760],
+               [3.8920, -35.1300, 24.8000],
+               [10.3400, -92.7500, 66.8000],
+               [7.2030, -61.5900, 38.3400],
+               [4.4540, -36.8300, 20.2900],
+               [1.9710, -15.5400, 6.9370],
+               [3.7730, -30.2800, 14.6900]]) * 0.001,
+         np.diag([50, 0, 0, 0, 50, 0, 0, 0, 0]),
+         np.eye(3),
+         None),
+        # darex #12
+        (np.array([[0, 1e6],[0, 0]]),
+         np.array([[0],[1]]),
+         np.eye(2),
+         np.array([[1]]),
+         "Bad absolute accuracy"),
+        # darex #13
+        (np.array([[16, 10, -2],
+                  [10, 13, -8],
+                  [-2, -8, 7]]) * (1/9),
+         np.eye(3),
+         1e6 * np.eye(3),
+         1e6 * np.eye(3),
+         "Fails to find a valid solution"),
+        # darex #14
+        (np.array([[1 - 1/1e8, 0, 0, 0],
+                  [1, 0, 0, 0],
+                  [0, 1, 0, 0],
+                  [0, 0, 1, 0]]),
+         np.array([[1e-08],[0],[0],[0]]),
+         np.diag([0, 0, 0, 1]),
+         np.array([[0.25]]),
+         "Bad absolute accuracy"),
+        # darex #15
+        (np.eye(100,k=1),
+         np.flipud(np.eye(100,1)),
+         np.eye(100),
+         np.array([[1]]),
+         "Fails to find a valid solution")
         ]
 
-    def check_case(self, a, b, q, r):
+    def _test_factory(case):
         """Checks if X = A'XA-(A'XB)(R+B'XB)^-1(B'XA)+Q) is true"""
+        a, b, q, r, knownfailure = case
+        if knownfailure:
+            raise KnownFailureTest(knownfailure)
 
         x = solve_discrete_are(a, b, q, r)
-        assert_array_almost_equal(
-            a.getH()*x*a-(a.getH()*x*b)*inv(r+b.getH()*x*b)*(b.getH()*x*a)+q-x, 0.0)
+        res = a.conj().T.dot(x.dot(a)) - x + q
+        res -= a.conj().T.dot(x.dot(b)).dot(
+                    solve(r+b.conj().T.dot(x.dot(b)),b.conj().T).dot(x.dot(a))
+                    )
+        assert_array_almost_equal(res,np.zeros_like(res))
 
-    def test_cases(self):
-        for case in self.cases:
-            self.check_case(case[0], case[1], case[2], case[3])
+    for case in cases:
+        yield _test_factory, case
 
 
 class TestSolveSylvester(TestCase):