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.
- directsd/__init__.py +141 -0
- directsd/analysis/__init__.py +4 -0
- directsd/analysis/charpol.py +89 -0
- directsd/analysis/errors.py +185 -0
- directsd/analysis/norms.py +630 -0
- directsd/design/__init__.py +9 -0
- directsd/design/convex.py +620 -0
- directsd/design/lifting.py +403 -0
- directsd/design/polynomial.py +5925 -0
- directsd/examples/__init__.py +0 -0
- directsd/examples/_common.py +36 -0
- directsd/examples/demos.py +1161 -0
- directsd/examples/examples.py +296 -0
- directsd/examples/help_examples.py +1043 -0
- directsd/glopt/__init__.py +4 -0
- directsd/glopt/advanced.py +1658 -0
- directsd/glopt/optimize.py +420 -0
- directsd/linalg/__init__.py +3 -0
- directsd/linalg/linsys.py +66 -0
- directsd/linalg/matrices.py +156 -0
- directsd/linalg/minreal.py +425 -0
- directsd/linalg/riccati.py +197 -0
- directsd/polynomial/__init__.py +11 -0
- directsd/polynomial/diophantine.py +426 -0
- directsd/polynomial/operations.py +247 -0
- directsd/polynomial/poln.py +742 -0
- directsd/polynomial/spectral.py +368 -0
- directsd/polynomial/transforms.py +124 -0
- directsd/polynomial/utils.py +449 -0
- directsd/sspace/__init__.py +4 -0
- directsd/sspace/design.py +1083 -0
- directsd/sspace/plant.py +198 -0
- directsd/tf/__init__.py +9 -0
- directsd/tf/interconnect.py +105 -0
- directsd/zpk/__init__.py +1 -0
- directsd/zpk/zpk.py +400 -0
- directsd_python-0.1.0.dist-info/METADATA +450 -0
- directsd_python-0.1.0.dist-info/RECORD +41 -0
- directsd_python-0.1.0.dist-info/WHEEL +5 -0
- directsd_python-0.1.0.dist-info/licenses/LICENSE +36 -0
- 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,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)
|