DirectSD-Python 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. directsd/__init__.py +141 -0
  2. directsd/analysis/__init__.py +4 -0
  3. directsd/analysis/charpol.py +89 -0
  4. directsd/analysis/errors.py +185 -0
  5. directsd/analysis/norms.py +630 -0
  6. directsd/design/__init__.py +9 -0
  7. directsd/design/convex.py +620 -0
  8. directsd/design/lifting.py +403 -0
  9. directsd/design/polynomial.py +5925 -0
  10. directsd/examples/__init__.py +0 -0
  11. directsd/examples/_common.py +36 -0
  12. directsd/examples/demos.py +1161 -0
  13. directsd/examples/examples.py +296 -0
  14. directsd/examples/help_examples.py +1043 -0
  15. directsd/glopt/__init__.py +4 -0
  16. directsd/glopt/advanced.py +1658 -0
  17. directsd/glopt/optimize.py +420 -0
  18. directsd/linalg/__init__.py +3 -0
  19. directsd/linalg/linsys.py +66 -0
  20. directsd/linalg/matrices.py +156 -0
  21. directsd/linalg/minreal.py +425 -0
  22. directsd/linalg/riccati.py +197 -0
  23. directsd/polynomial/__init__.py +11 -0
  24. directsd/polynomial/diophantine.py +426 -0
  25. directsd/polynomial/operations.py +247 -0
  26. directsd/polynomial/poln.py +742 -0
  27. directsd/polynomial/spectral.py +368 -0
  28. directsd/polynomial/transforms.py +124 -0
  29. directsd/polynomial/utils.py +449 -0
  30. directsd/sspace/__init__.py +4 -0
  31. directsd/sspace/design.py +1083 -0
  32. directsd/sspace/plant.py +198 -0
  33. directsd/tf/__init__.py +9 -0
  34. directsd/tf/interconnect.py +105 -0
  35. directsd/zpk/__init__.py +1 -0
  36. directsd/zpk/zpk.py +400 -0
  37. directsd_python-0.1.0.dist-info/METADATA +450 -0
  38. directsd_python-0.1.0.dist-info/RECORD +41 -0
  39. directsd_python-0.1.0.dist-info/WHEEL +5 -0
  40. directsd_python-0.1.0.dist-info/licenses/LICENSE +36 -0
  41. directsd_python-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,420 @@
1
+ """
2
+ Global optimization routines for DirectSD.
3
+
4
+ Ports of: simanneal, neldermead, randsearch, optglob, sector
5
+ """
6
+
7
+ import numpy as np
8
+
9
+
10
+ # ---------------------------------------------------------------------------
11
+ # Nelder-Mead simplex
12
+ # ---------------------------------------------------------------------------
13
+
14
+ def neldermead(func, x0, tol=1e-5, max_iter=1000, max_feval=5000):
15
+ """
16
+ Simplex minimization by Nelder and Mead.
17
+
18
+ Parameters
19
+ ----------
20
+ func : callable
21
+ Objective function f(x) -> float.
22
+ x0 : array-like
23
+ Initial guess.
24
+ tol : float
25
+ Convergence tolerance on x.
26
+ max_iter : int
27
+ max_feval : int
28
+
29
+ Returns
30
+ -------
31
+ x_best : np.ndarray
32
+ f_best : float
33
+ n_evals : int
34
+ """
35
+ x0 = np.array(x0, dtype=float).ravel()
36
+ n = len(x0)
37
+
38
+ rho, chi, psi, sigma = 1.0, 2.0, 0.5, 0.5
39
+
40
+ # Build initial simplex
41
+ X = np.zeros((n, n + 1))
42
+ X[:, 0] = x0
43
+ y = np.zeros(n + 1)
44
+ y[0] = func(x0)
45
+ n_evals = 1
46
+
47
+ for j in range(n):
48
+ z = x0.copy()
49
+ z[j] = z[j] * 1.05 if z[j] != 0 else 0.00025
50
+ X[:, j + 1] = z
51
+ y[j + 1] = func(z)
52
+ n_evals += 1
53
+
54
+ for _ in range(max_iter):
55
+ if n_evals >= max_feval:
56
+ break
57
+
58
+ # Sort
59
+ order = np.argsort(y)
60
+ y = y[order]
61
+ X = X[:, order]
62
+
63
+ # Convergence check
64
+ if np.max(np.abs(X[:, 1:] - X[:, :1])) < tol:
65
+ break
66
+
67
+ # Centroid of best n points
68
+ xbar = X[:, :n].mean(axis=1)
69
+ xr = (1 + rho) * xbar - rho * X[:, n] # reflection
70
+ fr = func(xr); n_evals += 1
71
+
72
+ if fr < y[0]:
73
+ # Expansion
74
+ xe = (1 + rho * chi) * xbar - rho * chi * X[:, n]
75
+ fe = func(xe); n_evals += 1
76
+ if fe < fr:
77
+ X[:, n] = xe; y[n] = fe
78
+ else:
79
+ X[:, n] = xr; y[n] = fr
80
+ elif fr < y[n - 1]:
81
+ X[:, n] = xr; y[n] = fr
82
+ else:
83
+ if fr < y[n]:
84
+ # Outside contraction
85
+ xc = (1 + psi * rho) * xbar - psi * rho * X[:, n]
86
+ else:
87
+ # Inside contraction
88
+ xc = (1 - psi) * xbar + psi * X[:, n]
89
+ fc = func(xc); n_evals += 1
90
+ if fc < min(fr, y[n]):
91
+ X[:, n] = xc; y[n] = fc
92
+ else:
93
+ # Shrink
94
+ for j in range(1, n + 1):
95
+ X[:, j] = X[:, 0] + sigma * (X[:, j] - X[:, 0])
96
+ y[j] = func(X[:, j]); n_evals += 1
97
+
98
+ order = np.argsort(y)
99
+ return X[:, order[0]], y[order[0]], n_evals
100
+
101
+
102
+ # ---------------------------------------------------------------------------
103
+ # Simulated Annealing
104
+ # ---------------------------------------------------------------------------
105
+
106
+ def simanneal(func, x0, options=None, constraint_func=None, proj_func=None):
107
+ """
108
+ Direct search using simulated annealing.
109
+
110
+ Parameters
111
+ ----------
112
+ func : callable
113
+ Cost function f(x) -> float.
114
+ x0 : array-like
115
+ Initial point.
116
+ options : dict, optional
117
+ Fields: display, tol, maxFunEvals, iniStep, multiStep,
118
+ maxFail, decStepBy, startTemp, tempDecRate, adaptRate.
119
+ constraint_func : callable, optional
120
+ g(x) -> array; values should be <= 0 for feasibility.
121
+ proj_func : callable, optional
122
+ proj(x) -> x_projected.
123
+
124
+ Returns
125
+ -------
126
+ x_best : np.ndarray
127
+ f_best : float
128
+ n_evals : int
129
+ x_trace : list of np.ndarray
130
+ """
131
+ defaults = dict(
132
+ display='off',
133
+ tol=1e-4,
134
+ maxFunEvals=1000,
135
+ iniStep=0.1,
136
+ multiStep=20,
137
+ maxFail=100,
138
+ decStepBy=0.5,
139
+ decFailRate=1.1,
140
+ adaptRate=0.1,
141
+ startTemp=100.0,
142
+ tempDecRate=0.95,
143
+ dispIter=100,
144
+ )
145
+ if options:
146
+ defaults.update(options)
147
+ opt = defaults
148
+
149
+ x0 = np.array(x0, dtype=float).ravel()
150
+ n = len(x0)
151
+
152
+ x_cur = x0.copy()
153
+ if proj_func:
154
+ x_cur = proj_func(x_cur)
155
+
156
+ f_cur = func(x_cur)
157
+ x_best = x_cur.copy()
158
+ f_best = f_cur
159
+ n_evals = 1
160
+
161
+ step = opt['iniStep'] * (np.max(np.abs(x0)) + 1.0) # scale step to problem
162
+ T = opt['startTemp']
163
+ # Auto-scale temperature if too high relative to objective
164
+ if abs(f_cur) > 0:
165
+ T = min(T, abs(f_cur) * 10)
166
+ fail = 0
167
+ x_trace = [x_cur.copy()]
168
+
169
+ for iteration in range(opt['maxFunEvals'] - 1):
170
+ for _ in range(opt['multiStep']):
171
+ # Propose new point
172
+ x_new = x_cur + step * (2 * np.random.rand(n) - 1)
173
+ if proj_func:
174
+ x_new = proj_func(x_new)
175
+
176
+ # Check constraints
177
+ if constraint_func and np.any(constraint_func(x_new) > 0):
178
+ fail += 1
179
+ continue
180
+
181
+ f_new = func(x_new)
182
+ n_evals += 1
183
+
184
+ dE = f_new - f_cur
185
+ if dE < 0 or (T > 1e-10 and np.random.rand() < np.exp(-dE / T)):
186
+ x_cur = x_new
187
+ f_cur = f_new
188
+ fail = 0
189
+ if f_cur < f_best:
190
+ x_best = x_cur.copy()
191
+ f_best = f_cur
192
+ x_trace.append(x_best.copy())
193
+ else:
194
+ fail += 1
195
+
196
+ if fail >= opt['maxFail']:
197
+ step *= opt['decStepBy']
198
+ opt['maxFail'] = int(opt['maxFail'] * opt['decFailRate'])
199
+ fail = 0
200
+
201
+ T *= opt['tempDecRate']
202
+
203
+ if opt['display'] == 'on' and (iteration + 1) % opt['dispIter'] == 0:
204
+ print(f" Iter {iteration + 1}: f_best = {f_best:.6g}, step = {step:.4g}, T = {T:.4g}")
205
+
206
+ if n_evals >= opt['maxFunEvals']:
207
+ break
208
+ if step < opt['tol']:
209
+ break
210
+
211
+ if opt['display'] in ('on', 'final'):
212
+ print(f"SA finished: f_best = {f_best:.6g}, evals = {n_evals}")
213
+
214
+ return x_best, f_best, n_evals, x_trace
215
+
216
+
217
+ # ---------------------------------------------------------------------------
218
+ # Random search
219
+ # ---------------------------------------------------------------------------
220
+
221
+ def randsearch(func, x0, bounds=None, n_iter=500, seed=None):
222
+ """
223
+ Simple random search optimization.
224
+
225
+ Parameters
226
+ ----------
227
+ func : callable
228
+ x0 : array-like
229
+ Initial guess.
230
+ bounds : list of (min, max) tuples, optional
231
+ Search bounds per dimension.
232
+ n_iter : int
233
+ seed : int, optional
234
+
235
+ Returns
236
+ -------
237
+ x_best, f_best, n_evals
238
+ """
239
+ rng = np.random.default_rng(seed)
240
+ x0 = np.array(x0, dtype=float).ravel()
241
+ n = len(x0)
242
+
243
+ x_best = x0.copy()
244
+ f_best = func(x0)
245
+ n_evals = 1
246
+
247
+ for _ in range(n_iter - 1):
248
+ if bounds:
249
+ x = np.array([rng.uniform(lo, hi) for lo, hi in bounds])
250
+ else:
251
+ x = x_best + rng.normal(0, 0.1, n)
252
+
253
+ f = func(x)
254
+ n_evals += 1
255
+ if f < f_best:
256
+ f_best = f
257
+ x_best = x.copy()
258
+
259
+ return x_best, f_best, n_evals
260
+
261
+
262
+ # ---------------------------------------------------------------------------
263
+ # Controlled Random Search (CRS)
264
+ # ---------------------------------------------------------------------------
265
+
266
+ def crandsearch(func, bounds, n_pop=None, max_feval=10000, tol=1e-6, seed=None):
267
+ """
268
+ Controlled Random Search (CRS) global optimization (Price, 1983).
269
+
270
+ Parameters
271
+ ----------
272
+ func : callable
273
+ Objective function f(x) -> float.
274
+ bounds : list of (lo, hi) tuples
275
+ Search domain bounds.
276
+ n_pop : int, optional
277
+ Population size (default: 10*n).
278
+ max_feval : int
279
+ tol : float
280
+ seed : int, optional
281
+
282
+ Returns
283
+ -------
284
+ x_best, f_best, n_evals
285
+ """
286
+ rng = np.random.default_rng(seed)
287
+ n = len(bounds)
288
+ if n_pop is None:
289
+ n_pop = 10 * n
290
+
291
+ lo = np.array([b[0] for b in bounds])
292
+ hi = np.array([b[1] for b in bounds])
293
+
294
+ # Initialize population
295
+ pop = rng.uniform(0, 1, (n_pop, n)) * (hi - lo) + lo
296
+ vals = np.array([func(p) for p in pop])
297
+ n_evals = n_pop
298
+
299
+ for _ in range(max_feval - n_pop):
300
+ # Select worst point
301
+ worst_idx = np.argmax(vals)
302
+
303
+ # Select n random distinct points (not worst)
304
+ candidates = [i for i in range(n_pop) if i != worst_idx]
305
+ sel = rng.choice(candidates, n, replace=False)
306
+
307
+ # Form trial point: centroid of selected minus worst
308
+ centroid = pop[sel].mean(axis=0)
309
+ x_new = 2 * centroid - pop[worst_idx]
310
+ x_new = np.clip(x_new, lo, hi)
311
+
312
+ f_new = func(x_new)
313
+ n_evals += 1
314
+
315
+ if f_new < vals[worst_idx]:
316
+ pop[worst_idx] = x_new
317
+ vals[worst_idx] = f_new
318
+
319
+ best_idx = np.argmin(vals)
320
+ if np.max(np.abs(pop - pop[best_idx])) < tol:
321
+ break
322
+
323
+ best_idx = np.argmin(vals)
324
+ return pop[best_idx], vals[best_idx], n_evals
325
+
326
+
327
+ # ---------------------------------------------------------------------------
328
+ # Sector (stability degree / oscillation)
329
+ # ---------------------------------------------------------------------------
330
+
331
+ def sector(poles, T=None):
332
+ """
333
+ Find degree of stability and oscillation for closed-loop poles.
334
+
335
+ Parameters
336
+ ----------
337
+ poles : array-like
338
+ Closed-loop poles (continuous or discrete).
339
+ T : float, optional
340
+ Sampling period (if None, treats poles as continuous-time).
341
+
342
+ Returns
343
+ -------
344
+ alpha : float
345
+ Degree of stability (distance from imaginary axis / unit circle).
346
+ beta : float
347
+ Degree of oscillation (max |imag part| / |real part|).
348
+ """
349
+ poles = np.atleast_1d(np.array(poles))
350
+
351
+ if T is not None and T > 0:
352
+ # Convert discrete poles to continuous
353
+ poles = np.log(poles) / T
354
+
355
+ real_parts = np.real(poles)
356
+ alpha = -np.max(real_parts) # positive = stable
357
+
358
+ with np.errstate(divide='ignore', invalid='ignore'):
359
+ osc = np.where(np.abs(real_parts) > 1e-10,
360
+ np.abs(np.imag(poles)) / np.abs(real_parts),
361
+ np.inf)
362
+ beta = np.max(osc[np.isfinite(osc)]) if np.any(np.isfinite(osc)) else 0.0
363
+
364
+ return float(alpha), float(beta)
365
+
366
+
367
+ # ---------------------------------------------------------------------------
368
+ # Utility: banana (Rosenbrock test function)
369
+ # ---------------------------------------------------------------------------
370
+
371
+ def banana(x):
372
+ """
373
+ Rosenbrock's banana function.
374
+ f(x, y) = 100*(y - x^2)^2 + (1 - x)^2
375
+ """
376
+ x = np.atleast_1d(x)
377
+ return 100 * (x[1] - x[0] ** 2) ** 2 + (1 - x[0]) ** 2
378
+
379
+
380
+ # ---------------------------------------------------------------------------
381
+ # dual_annealing – scipy-based replacement for simanneal (faster, more robust)
382
+ # ---------------------------------------------------------------------------
383
+
384
+ def dual_annealing(func, bounds, seed=None, maxiter=1000, **kwargs):
385
+ """
386
+ Global optimization using scipy.optimize.dual_annealing.
387
+
388
+ A hybrid approach combining classical simulated annealing with a fast
389
+ local search. Substantially faster and more reliable than the custom
390
+ simanneal() for reduced-order controller design (e.g. modsdh2).
391
+
392
+ Parameters
393
+ ----------
394
+ func : callable
395
+ Objective function f(x) -> float.
396
+ bounds : list of (lo, hi) tuples
397
+ Search bounds for each dimension.
398
+ seed : int, optional
399
+ Random seed for reproducibility.
400
+ maxiter : int
401
+ Maximum number of function evaluations.
402
+ **kwargs : dict
403
+ Passed directly to scipy.optimize.dual_annealing.
404
+
405
+ Returns
406
+ -------
407
+ x_best : np.ndarray
408
+ f_best : float
409
+ result : scipy.optimize.OptimizeResult
410
+ Full scipy result object (includes nfev, nit, message, etc.).
411
+
412
+ Example
413
+ -------
414
+ >>> from directsd.glopt import dual_annealing
415
+ >>> x, f, res = dual_annealing(banana, [(-3,3),(-3,3)], seed=42)
416
+ >>> print(f"minimum at x={x}, f={f:.2e}")
417
+ """
418
+ from scipy.optimize import dual_annealing as _scipy_da
419
+ result = _scipy_da(func, bounds, seed=seed, maxfun=maxiter, **kwargs)
420
+ return result.x, float(result.fun), result
@@ -0,0 +1,3 @@
1
+ from directsd.linalg.matrices import toep, hank, givens, house, lyap, dlyap
2
+ from directsd.linalg.linsys import linsys
3
+ from directsd.linalg.minreal import Minreal
@@ -0,0 +1,66 @@
1
+ """
2
+ Linear systems solver for DirectSD.
3
+
4
+ Port of MATLAB linsys.m: solves A*X = B using QR or SVD with optional
5
+ iterative refinement.
6
+ """
7
+
8
+ import numpy as np
9
+
10
+
11
+ def linsys(A, B, method='qr', refine=False):
12
+ """
13
+ Solve the linear system A * X = B.
14
+
15
+ Parameters
16
+ ----------
17
+ A : np.ndarray, shape (m, n)
18
+ B : np.ndarray, shape (m, k) or (m,)
19
+ method : str
20
+ 'qr' - QR decomposition (default)
21
+ 'svd' - SVD decomposition
22
+ refine : bool
23
+ If True, apply one step of iterative refinement.
24
+
25
+ Returns
26
+ -------
27
+ X : np.ndarray or None
28
+ Solution if found, else None.
29
+ """
30
+ A = np.atleast_2d(np.array(A, dtype=float))
31
+ B = np.atleast_1d(np.array(B, dtype=float))
32
+ if B.ndim == 1:
33
+ B = B.reshape(-1, 1)
34
+
35
+ tol = np.sqrt(np.finfo(float).eps)
36
+ m, n = A.shape
37
+
38
+ if method == 'svd':
39
+ U, s, Vt = np.linalg.svd(A, full_matrices=False)
40
+ rank = np.sum(s > tol * s[0])
41
+ if rank == 0:
42
+ return None
43
+ # Pseudo-inverse solution
44
+ s_inv = np.where(s > tol * s[0], 1.0 / s, 0.0)
45
+ X = Vt[:rank].T @ np.diag(s_inv[:rank]) @ U[:, :rank].T @ B
46
+
47
+ elif method == 'qr':
48
+ if m >= n:
49
+ Q, R = np.linalg.qr(A)
50
+ rank = np.sum(np.abs(np.diag(R)) > tol * max(np.abs(np.diag(R)).max(), 1.0))
51
+ if rank < n:
52
+ # Rank-deficient: fall back to SVD
53
+ return linsys(A, B, method='svd', refine=refine)
54
+ X = np.linalg.solve(R[:n, :n], Q[:, :n].T @ B)
55
+ else:
56
+ X, _, _, _ = np.linalg.lstsq(A, B, rcond=None)
57
+ else:
58
+ raise ValueError(f"Unknown method '{method}'")
59
+
60
+ if refine and X is not None:
61
+ # Iterative refinement step
62
+ r = B - A @ X
63
+ dX, _, _, _ = np.linalg.lstsq(A, r, rcond=None)
64
+ X = X + dX
65
+
66
+ return X
@@ -0,0 +1,156 @@
1
+ """
2
+ Linear algebra utilities for DirectSD.
3
+
4
+ Ports of: toep (Toeplitz matrix), hank (Hankel matrix), linsys utilities.
5
+ """
6
+
7
+ import numpy as np
8
+
9
+
10
+ def toep(a, r, c):
11
+ """
12
+ Build a Toeplitz matrix from polynomial coefficients.
13
+
14
+ The matrix has the structure:
15
+ | A(0) 0 0 ... |
16
+ | A(1) A(0) 0 ... |
17
+ | ... A(1) A(0) ... |
18
+
19
+ where A(0) is the leading (highest-degree) coefficient.
20
+
21
+ Parameters
22
+ ----------
23
+ a : array-like or Poln
24
+ Polynomial coefficients (highest degree first), or Poln object.
25
+ r : int
26
+ Number of rows.
27
+ c : int
28
+ Number of columns.
29
+
30
+ Returns
31
+ -------
32
+ T : np.ndarray, shape (r, c)
33
+ """
34
+ from directsd.polynomial.poln import Poln
35
+
36
+ if isinstance(a, Poln):
37
+ a = a.coef
38
+
39
+ a = np.atleast_1d(np.array(a, dtype=complex)).ravel()
40
+ # MATLAB toep does fliplr(a) before filling: constant term goes at top of each column.
41
+ a_asc = a[::-1] # ascending (constant first), matches MATLAB's fliplr convention
42
+
43
+ T = np.zeros((r, c), dtype=complex)
44
+ n = len(a_asc)
45
+ for j in range(c):
46
+ for i in range(r):
47
+ k = i - j
48
+ if 0 <= k < n:
49
+ T[i, j] = a_asc[k]
50
+
51
+ return np.real_if_close(T, tol=1e6)
52
+
53
+
54
+ def hank(a, r, c):
55
+ """
56
+ Build a Hankel matrix from polynomial coefficients.
57
+
58
+ Parameters
59
+ ----------
60
+ a : array-like
61
+ Polynomial coefficients.
62
+ r : int
63
+ Number of rows.
64
+ c : int
65
+ Number of columns.
66
+
67
+ Returns
68
+ -------
69
+ H : np.ndarray, shape (r, c)
70
+ """
71
+ a = np.atleast_1d(np.array(a, dtype=complex)).ravel()
72
+ H = np.zeros((r, c), dtype=complex)
73
+ for i in range(r):
74
+ for j in range(c):
75
+ k = i + j
76
+ if k < len(a):
77
+ H[i, j] = a[k]
78
+ return np.real_if_close(H, tol=1e6)
79
+
80
+
81
+ def givens(a, b):
82
+ """
83
+ Compute Givens rotation parameters (c, s) such that
84
+ [c s] [a] [r]
85
+ [-s c] [b] = [0]
86
+ """
87
+ if b == 0:
88
+ return 1.0, 0.0
89
+ elif abs(b) > abs(a):
90
+ tau = -a / b
91
+ s = 1.0 / np.sqrt(1 + tau ** 2)
92
+ c = s * tau
93
+ else:
94
+ tau = -b / a
95
+ c = 1.0 / np.sqrt(1 + tau ** 2)
96
+ s = c * tau
97
+ return c, s
98
+
99
+
100
+ def house(x):
101
+ """
102
+ Compute Householder vector v and scalar beta such that
103
+ (I - beta*v*v') * x = ||x|| * e1.
104
+ """
105
+ x = np.array(x, dtype=float).ravel()
106
+ sigma = np.dot(x[1:], x[1:])
107
+ v = x.copy()
108
+ v[0] = 1.0
109
+ if sigma == 0 and x[0] >= 0:
110
+ beta = 0.0
111
+ else:
112
+ mu = np.sqrt(x[0] ** 2 + sigma)
113
+ if x[0] <= 0:
114
+ v[0] = x[0] - mu
115
+ else:
116
+ v[0] = -sigma / (x[0] + mu)
117
+ beta = 2 * v[0] ** 2 / (sigma + v[0] ** 2)
118
+ v = v / v[0]
119
+ return v, beta
120
+
121
+
122
+ def schur_ordered(A, stable='lhp'):
123
+ """
124
+ Compute ordered Schur decomposition: T, Z such that A = Z T Z^H
125
+ with stable eigenvalues first.
126
+
127
+ Parameters
128
+ ----------
129
+ stable : str
130
+ 'lhp' for left-half-plane (continuous), 'unit' for inside unit circle (discrete).
131
+ """
132
+ import scipy.linalg as la
133
+
134
+ if stable == 'lhp':
135
+ sort_fn = lambda x: np.real(x) < 0
136
+ else:
137
+ sort_fn = lambda x: np.abs(x) < 1
138
+
139
+ T, Z, _ = la.schur(A, output='complex', sort=sort_fn)
140
+ return T, Z
141
+
142
+
143
+ def lyap(A, Q):
144
+ """
145
+ Solve continuous Lyapunov equation A*X + X*A' + Q = 0.
146
+ """
147
+ import scipy.linalg as la
148
+ return la.solve_continuous_lyapunov(A, -Q)
149
+
150
+
151
+ def dlyap(A, Q):
152
+ """
153
+ Solve discrete Lyapunov equation A*X*A' - X + Q = 0.
154
+ """
155
+ import scipy.linalg as la
156
+ return la.solve_discrete_lyapunov(A, Q)