@@ -47,15 +47,35 @@ def sb03md(n, C, A, U, dico, job='X', fact='N', trana='N', ldwork=None):
4747try :
4848 from slycot import sb04qd
4949except ImportError :
50- sb0qmd = None
50+ sb04qd = None
5151
5252try :
5353 from slycot import sg03ad
5454except ImportError :
55- sb04ad = None
55+ sg03ad = None
5656
5757__all__ = ['lyap' , 'dlyap' , 'dare' , 'care' ]
5858
59+
60+ def _warn_ill_conditioned_E (E ):
61+ """Warn that an ill-conditioned E costs accuracy.
62+
63+ The scipy generalized-Lyapunov fallback reduces the problem to a
64+ standard Lyapunov equation by inverting E, so a poorly conditioned E
65+ costs accuracy (continuous and discrete paths alike, regardless of
66+ whether the underlying scipy solve happens to warn). The generalized
67+ Lyapunov problem is itself ill-conditioned (about cond(E)**2) when E
68+ is, so method='slycot', though it does not form inv(E) explicitly, is
69+ not measurably more accurate in that regime.
70+ """
71+ condE = np .linalg .cond (E )
72+ if condE > 1.0 / np .sqrt (finfo (float ).eps ):
73+ warnings .warn (
74+ f"E is ill-conditioned (cond(E) = { condE :.2g} ); the generalized "
75+ "Lyapunov solution may have reduced accuracy. The problem itself "
76+ "is ill-conditioned for such E, so method='slycot' is not "
77+ "measurably more accurate." , UserWarning , stacklevel = 3 )
78+
5979#
6080# Lyapunov equation solvers lyap and dlyap
6181#
@@ -103,6 +123,25 @@ def lyap(A, Q, C=None, E=None, method=None):
103123 X : 2D array
104124 Solution to the Lyapunov or Sylvester equation.
105125
126+ Notes
127+ -----
128+ For the generalized Lyapunov equation, method='slycot' uses the
129+ SLICOT routine SG03AD, based on the generalized Schur method of
130+ Penzl [1]_, which factors the matrix pencil without inverting E.
131+ With method='scipy', the equation is transformed to a standard
132+ Lyapunov equation by inverting E, which requires E to be nonsingular
133+ and loses accuracy when E is ill-conditioned (a UserWarning is then
134+ issued). The generalized Lyapunov problem is itself ill-conditioned
135+ (about cond(E)**2) when E is, so method='slycot', though it does not
136+ invert E, is not measurably more accurate in that case. Both methods
137+ require E nonsingular; a truly singular
138+ (descriptor) E is not currently handled by either.
139+
140+ References
141+ ----------
142+ .. [1] Penzl, T., "Numerical solution of generalized Lyapunov
143+ equations", Advances in Computational Mathematics, 8:33-48, 1998.
144+
106145 """
107146 # Decide what method to use
108147 method = _slycot_or_scipy (method )
@@ -162,8 +201,25 @@ def lyap(A, Q, C=None, E=None, method=None):
162201 _check_shape (E , n , n , square = True , name = "E" )
163202
164203 if method == 'scipy' :
165- raise ControlArgument (
166- "method='scipy' not valid for generalized Lyapunov equation" )
204+ # Transform to a standard Lyapunov equation by multiplying
205+ # from the left by inv(E) and from the right by inv(E).T:
206+ #
207+ # (E^-1 A) X + X (E^-1 A)^T + E^-1 Q E^-T = 0
208+ #
209+ # This requires E to be nonsingular. SG03AD (method='slycot',
210+ # Penzl's generalized Schur method) factors the pencil without
211+ # inverting E, but a truly singular E is not handled by either
212+ # method.
213+ try :
214+ At = solve (E , A )
215+ Qt = solve (E , solve (E , Q ).T ).T
216+ except np .linalg .LinAlgError :
217+ raise ControlArgument (
218+ "method='scipy' requires E to be nonsingular; "
219+ "a truly singular E (descriptor system) is not "
220+ "supported by either method" )
221+ _warn_ill_conditioned_E (E )
222+ return sp .linalg .solve_continuous_lyapunov (At , - Qt )
167223
168224 # Make sure we have access to the write Slycot routine
169225 try :
@@ -229,6 +285,36 @@ def dlyap(A, Q, C=None, E=None, method=None):
229285 X : 2D array (or matrix)
230286 Solution to the Lyapunov or Sylvester equation.
231287
288+ Notes
289+ -----
290+ For the generalized Lyapunov equation, method='slycot' uses the
291+ SLICOT routine SG03AD, based on the generalized Schur method of
292+ Penzl [1]_, which factors the matrix pencil without inverting E.
293+ With method='scipy', the equation is transformed to a standard
294+ Lyapunov equation by inverting E, which requires E to be nonsingular
295+ and loses accuracy when E is ill-conditioned (a UserWarning is then
296+ issued). The generalized Lyapunov problem is itself ill-conditioned
297+ (about cond(E)**2) when E is, so method='slycot', though it does not
298+ invert E, is not measurably more accurate in that case. Both methods
299+ require E nonsingular; a truly singular
300+ (descriptor) E is not currently handled by either.
301+
302+ For the Sylvester equation, method='slycot' uses the
303+ Hessenberg-Schur method of the SLICOT routine SB04QD [2]_ and
304+ method='scipy' uses the Bartels-Stewart method [3]_; both reduce the
305+ coefficient matrices to (Hessenberg-)Schur form and solve the result
306+ by back-substitution, with O(n^3 + m^3) cost.
307+
308+ References
309+ ----------
310+ .. [1] Penzl, T., "Numerical solution of generalized Lyapunov
311+ equations", Advances in Computational Mathematics, 8:33-48, 1998.
312+ .. [2] Golub, G.H., Nash, S., and Van Loan, C., "A Hessenberg-Schur
313+ method for the problem AX + XB = C", IEEE Trans. Automatic
314+ Control, AC-24, pp. 909-913, 1979.
315+ .. [3] Bartels, R.H. and Stewart, G.W., "Solution of the matrix
316+ equation AX + XB = C", Comm. ACM, 15(9), pp. 820-826, 1972.
317+
232318 """
233319 # Decide what method to use
234320 method = _slycot_or_scipy (method )
@@ -279,8 +365,40 @@ def dlyap(A, Q, C=None, E=None, method=None):
279365 _check_shape (C , n , m , name = "C" )
280366
281367 if method == 'scipy' :
282- raise ControlArgument (
283- "method='scipy' not valid for Sylvester equation" )
368+ # Solve the discrete-time Sylvester equation
369+ #
370+ # A X Q^T - X + C = 0
371+ #
372+ # by the Bartels-Stewart method, matching the complexity of
373+ # the Hessenberg-Schur algorithm of the SLICOT routine
374+ # SB04QD used by method='slycot' (Golub, Nash, and Van
375+ # Loan, 1979): with complex Schur forms A = U Ta U^H and
376+ # Q^T = V Tq V^H and Y = U^H X V, the transformed equation
377+ # Ta Y Tq - Y + U^H C V = 0 is solved column by column,
378+ # each column requiring one triangular solve. O(n^3 + m^3)
379+ # flops overall.
380+ Ta , U = sp .linalg .schur (A , output = 'complex' )
381+ Tq , V = sp .linalg .schur (Q .T , output = 'complex' )
382+ Ct = U .conj ().T @ C @ V
383+ # Solvability requires lam_A * lam_Q != 1 for all pairs of
384+ # eigenvalues (the diagonals of the triangular factors)
385+ if np .min (np .abs (np .outer (np .diag (Tq ), np .diag (Ta )) - 1. )) \
386+ < finfo (float ).eps * max (
387+ 1. , np .abs (np .diag (Ta )).max ()
388+ * np .abs (np .diag (Tq )).max ()):
389+ raise ControlArgument (
390+ "A and Q have a pair of eigenvalues whose product "
391+ "is (almost) equal to 1; the discrete-time "
392+ "Sylvester equation is singular" )
393+ Y = np .empty ((n , m ), dtype = complex )
394+ TaY = np .empty ((n , m ), dtype = complex ) # running Ta @ Y
395+ In = np .eye (n )
396+ for k in range (m ):
397+ rhs = - Ct [:, k ] - TaY [:, :k ] @ Tq [:k , k ]
398+ Y [:, k ] = sp .linalg .solve_triangular (
399+ Tq [k , k ] * Ta - In , rhs )
400+ TaY [:, k ] = Ta @ Y [:, k ]
401+ return np .real (U @ Y @ V .conj ().T )
284402
285403 # Solve the Sylvester equation by calling Slycot function sb04qd
286404 X = sb04qd (n , m , - A , Q .T , C )
@@ -292,8 +410,25 @@ def dlyap(A, Q, C=None, E=None, method=None):
292410 _check_shape (E , n , n , square = True , name = "E" )
293411
294412 if method == 'scipy' :
295- raise ControlArgument (
296- "method='scipy' not valid for generalized Lyapunov equation" )
413+ # Transform to a standard Lyapunov equation by multiplying
414+ # from the left by inv(E) and from the right by inv(E).T:
415+ #
416+ # (E^-1 A) X (E^-1 A)^T - X + E^-1 Q E^-T = 0
417+ #
418+ # This requires E to be nonsingular. SG03AD (method='slycot',
419+ # Penzl's generalized Schur method) factors the pencil without
420+ # inverting E, but a truly singular E is not handled by either
421+ # method.
422+ try :
423+ At = solve (E , A )
424+ Qt = solve (E , solve (E , Q ).T ).T
425+ except np .linalg .LinAlgError :
426+ raise ControlArgument (
427+ "method='scipy' requires E to be nonsingular; "
428+ "a truly singular E (descriptor system) is not "
429+ "supported by either method" )
430+ _warn_ill_conditioned_E (E )
431+ return sp .linalg .solve_discrete_lyapunov (At , Qt )
297432
298433 # Solve the generalized Lyapunov equation by calling Slycot
299434 # function sg03ad
0 commit comments