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,1161 @@
1
+ """
2
+ DirectSD Python Toolbox – Demo Scripts
3
+ =======================================
4
+ Faithful ports of the 25 MATLAB dsddemos/*.m scripts.
5
+
6
+ Each demo is a self-contained function. Run all of them via::
7
+
8
+ python -m directsd.examples.demos
9
+
10
+ Or run a single demo::
11
+
12
+ python -c "from directsd.examples.demos import demo_doubint; demo_doubint()"
13
+
14
+ Differences from MATLAB originals
15
+ ----------------------------------
16
+ * Simulink-based transient simulations are replaced with
17
+ ``scipy.signal.lsim`` (CT) or the ``sdsim`` function.
18
+ * MATLAB ``pause`` / interactive prompts are omitted.
19
+ * ``z2zeta`` (ζ = 1/z substitution) is handled inline where needed.
20
+ * ``sdh2norm(sys, K, t_array)`` (inter-sample variance curve) is not yet
21
+ implemented in Python; average H2-norm is shown instead.
22
+ * ``modsdh2``/``modsdl2`` require a SISO P22 plant; full MIMO cost is
23
+ computed via ``sdh2``/``sdl2`` on the standard system.
24
+ * ``ch2`` returns degenerate results for plants with integrators;
25
+ CT-redesign sections use ``bilintr`` only when ``ch2`` succeeds.
26
+ """
27
+
28
+ from __future__ import annotations
29
+ import math
30
+ import numpy as np
31
+ import scipy.signal as sig
32
+
33
+ from directsd import (
34
+ sdh2, sdl2, ch2, dhinf, sdahinf, sdtrhinf,
35
+ sdh2hinf, modsdh2, modsdl2, sd2dof, split2dof,
36
+ sdh2norm, sdhinorm, sdahinorm, dinfnorm,
37
+ sdl2err, sd2doferr, sdtrhinferr,
38
+ sdmargin,
39
+ sfactor, bilintr,
40
+ neldermead, simanneal, sector, banana,
41
+ h2reg, sdh2reg, sdfast, sdh2simple, sdnorm,
42
+ sdsim,
43
+ )
44
+ from directsd import GeneralizedPlant
45
+ from directsd.tf import mul, neg, add, feedback, nd, to_lti
46
+ from directsd.examples._common import cl_poles as _cl_poles, _z2zeta
47
+
48
+
49
+ # ── Shared helpers ────────────────────────────────────────────────────────────
50
+
51
+ def _sep(title: str) -> None:
52
+ print(f"\n{'='*56}")
53
+ print(f" {title}")
54
+ print('='*56)
55
+
56
+
57
+ def _hdr(title: str) -> None:
58
+ print(f"\n{'#'*60}")
59
+ print(f" DirectSD Demo: {title}")
60
+ print(f"{'#'*60}")
61
+
62
+
63
+
64
+ def _K_ss(K, T):
65
+ """Convert (num, den) controller tuple → discrete StateSpace with dt=T."""
66
+ return sig.StateSpace(*sig.dlti(K[0], K[1], dt=T).to_ss())
67
+
68
+
69
+ def _hinf(plant, K, T):
70
+ """sdhinorm returns (norm, freq); extract norm only."""
71
+ return sdhinorm(plant, K, T)[0]
72
+
73
+
74
+ def _tf_nd(K_ss):
75
+ """Extract (num, den) 1-D arrays from SISO discrete StateSpace (h2reg output)."""
76
+ tf = K_ss.to_tf()
77
+ n, d = tf.num, tf.den
78
+ # SISO: num/den are 1-D arrays; MIMO: 2-D object arrays
79
+ if isinstance(n, np.ndarray) and n.dtype == object:
80
+ return np.atleast_1d(n[0][0]).ravel(), np.atleast_1d(d[0][0]).ravel()
81
+ return np.atleast_1d(n).ravel(), np.atleast_1d(d).ravel()
82
+
83
+
84
+ # ── demo_doubint ──────────────────────────────────────────────────────────────
85
+
86
+ def demo_doubint():
87
+ """Optimal control for double integrator (Polyakov et al., CCA 2002)."""
88
+ _hdr("Optimal digital control of double integrator")
89
+
90
+ F = sig.lti([1], [1, 0, 0]) # 1/s^2
91
+ T = 0.1
92
+
93
+ _sep("Sampled-data H2-optimisation")
94
+ Kopt, err_opt = sdh2(F, T)
95
+ print(f"H2-optimal controller: {np.round(Kopt[0],4)} / {np.round(Kopt[1],4)}")
96
+ poles = _cl_poles(F, Kopt, T)
97
+ print(f"Closed-loop poles: {np.round(poles, 4)}")
98
+ # sdh2norm has numerical issues for double integrator; use design cost
99
+ print(f"Sampled-data H2 cost: {err_opt:.6f}")
100
+
101
+ _sep("Continuous-time H2-optimisation (Tustin redesign)")
102
+ Kc, cost_ct = ch2(F)
103
+ # ch2 may return degenerate result for pure integrators
104
+ try:
105
+ n, d = Kc
106
+ if len(np.atleast_1d(d).ravel()) <= 1 and np.allclose(np.atleast_1d(d).ravel(), 0):
107
+ print("CT H2 degenerate for pure double integrator (improper cost).")
108
+ else:
109
+ Kcd = bilintr(Kc, 'tustin', T)
110
+ poles_c2d = _cl_poles(F, Kcd, T)
111
+ print(f"CT H2 → Tustin K: {np.round(Kcd[0],4)} / {np.round(Kcd[1],4)}")
112
+ print(f"Closed-loop poles: {np.round(poles_c2d, 4)}")
113
+ err_c2d = sdh2norm(F, Kcd, T)
114
+ print(f"Sampled-data H2 cost: {err_c2d:.6f}")
115
+ except Exception as exc:
116
+ print(f" (CT redesign skipped: {exc})")
117
+
118
+ _sep("H2-optimisation for ZOH-discretised model")
119
+ F_ss = sig.lti([1], [1, 0, 0]).to_ss()
120
+ # Measurement row uses -C (P22 = -F) to match negative-feedback convention
121
+ # (consistent with _siso_to_gen_plant and MATLAB's sys = [[-F,-F],[0,1],[-F,-F]])
122
+ sys_gen = sig.StateSpace(
123
+ F_ss.A,
124
+ np.hstack([F_ss.B, F_ss.B]),
125
+ np.vstack([F_ss.C, np.zeros((1, F_ss.A.shape[0])), -F_ss.C]),
126
+ np.array([[0., 0.], [0., 1.], [0., 0.]])
127
+ )
128
+ dsys_d = sys_gen.to_discrete(T, method='zoh')
129
+ Kd_ss, h2n_d = h2reg(dsys_d, n_meas=1, n_ctrl=1)
130
+ Kd = _tf_nd(Kd_ss)
131
+ print(f"Discrete-time K: {np.round(Kd[0],4)} / {np.round(Kd[1],4)}")
132
+ poles_d = _cl_poles(F, Kd, T)
133
+ print(f"Closed-loop poles: {np.round(poles_d, 4)}")
134
+ err_d = sdh2norm(F, Kd, T)
135
+ print(f"Sampled-data H2 cost: {err_d:.6f}")
136
+
137
+ print(f"\nSummary (H2 cost, lower is better):")
138
+ print(f" Sampled-data design: {err_opt:.6f}")
139
+ print(f" Discrete-time design: {err_d:.6f}")
140
+
141
+
142
+ # ── demo_ait98 ────────────────────────────────────────────────────────────────
143
+
144
+ def demo_ait98():
145
+ """H2 and AHinf-optimisation (Polyakov, ARC 1998)."""
146
+ _hdr("H2 and AHinf-optimisation of sampled-data system")
147
+
148
+ F2 = sig.lti([1], [1, -1]) # 1/(s-1) unstable plant
149
+ T = 1
150
+
151
+ _sep("H2-optimisation (SISO plant, standard form)")
152
+ K, err_opt = sdh2(F2, T)
153
+ print(f"H2-optimal controller: {np.round(K[0],4)} / {np.round(K[1],4)}")
154
+ poles = _cl_poles(F2, K, T)
155
+ print(f"Closed-loop poles: {np.round(poles, 4)}")
156
+ err = sdh2norm(F2, K, T)
157
+ print(f"Sampled-data H2 cost: {err:.6f}")
158
+ lam = sdahinorm(F2, K, T)
159
+ print(f"AHinf-cost: {lam:.6f}")
160
+ lam_inf = _hinf(F2, K, T)
161
+ print(f"Hinf-norm: {lam_inf:.6f}")
162
+
163
+ _sep("AHinf-optimisation")
164
+ Kinf, lam_opt = sdahinf(F2, T)
165
+ print(f"AHinf-optimal K: {np.round(Kinf[0],4)} / {np.round(Kinf[1],4)}")
166
+ poles2 = _cl_poles(F2, Kinf, T)
167
+ print(f"Closed-loop poles: {np.round(poles2, 4)}")
168
+ err2 = sdh2norm(F2, Kinf, T)
169
+ print(f"H2 cost: {err2:.6f}")
170
+ print(f"Optimal AHinf-cost: {lam_opt:.6f}")
171
+ lam_d = sdahinorm(F2, Kinf, T)
172
+ print(f"AHinf-cost (verify): {lam_d:.6f}")
173
+ lam_inf2 = _hinf(F2, Kinf, T)
174
+ print(f"Hinf-norm: {lam_inf2:.6f}")
175
+
176
+
177
+ # ── demo_at96 ─────────────────────────────────────────────────────────────────
178
+
179
+ def demo_at96():
180
+ """Optimal ship course stabilisation – 'Kazbek' tanker (AT 1996)."""
181
+ _hdr("Stochastic optimisation for 'Kazbek' type tanker")
182
+
183
+ F = sig.lti([0.051], [25, 1, 0])
184
+ rho = np.sqrt(0.1)
185
+ Sw = sig.lti([0.0757], [1, 0, 2.489, 0, 1.848])
186
+ Fw, _ = sfactor(Sw)
187
+ Fw_lti = to_lti(Fw) # ensure TF form (sfactor may return ZPK)
188
+ T = 1
189
+
190
+ def _sys(rho_):
191
+ return GeneralizedPlant([
192
+ [mul(F, Fw_lti), F], # z1
193
+ [0, rho_], # z2
194
+ [neg(mul(F, Fw_lti)), neg(F)], # y
195
+ ])
196
+
197
+ _sep("H2-optimal controller")
198
+ sys = _sys(rho)
199
+ K, err_opt = sdh2(sys, T)
200
+ print(f"H2-optimal K: {np.round(K[0],4)} / {np.round(K[1],4)}")
201
+ print(f"Optimal H2 cost: {err_opt:.6f}")
202
+ err_v = sdh2norm(sys, K, T)
203
+ print(f"Direct computation: {err_v:.6f}")
204
+
205
+ _sep("Trade-off curve sigma_psi vs sigma_u (7 rho values)")
206
+ rr = np.array([0.001, 0.003, 0.01, 0.03, 0.1, 0.3, 1.0])
207
+ print(f" {'rho':>8} {'H2 cost':>12}")
208
+ for r in rr:
209
+ sys_r = _sys(np.sqrt(r))
210
+ K_r, J_r = sdh2(sys_r, T)
211
+ print(f" {r:>8.4f} {J_r:>12.6f}")
212
+
213
+
214
+ # ── demo_autom97 ──────────────────────────────────────────────────────────────
215
+
216
+ def demo_autom97():
217
+ """H2-optimal control for delayed plants (Automatica 1997)."""
218
+ _hdr("Stochastic optimisation with time-delay")
219
+
220
+ F = sig.lti([1, 0.5], [1, -1, 0])
221
+ Fw = sig.lti([2], [1, 2])
222
+ T = 0.2
223
+
224
+ print(f"Plant: (s+0.5)/(s^2-s), disturbance: 2/(s+2)")
225
+ print(f"Sampling period T={T} (input delay tau≈0.093 not modelled here)")
226
+
227
+ sys = GeneralizedPlant([
228
+ [mul(F, Fw), F],
229
+ [0, 0],
230
+ [neg(mul(F, Fw)), neg(F)],
231
+ ])
232
+
233
+ _sep("Sampled-data H2-optimisation")
234
+ K, err_opt = sdh2(sys, T)
235
+ print(f"H2-optimal K: {np.round(K[0],4)} / {np.round(K[1],4)}")
236
+ print(f"Optimal H2 cost: {err_opt:.6f}")
237
+ err = sdh2norm(sys, K, T)
238
+ print(f"Average variance: {err:.6f}")
239
+ print(f"Note: inter-sample variance curve not computed (Python limitation)")
240
+
241
+
242
+ # ── demo_c2d ──────────────────────────────────────────────────────────────────
243
+
244
+ def demo_c2d():
245
+ """Optimal digital redesign – Rattan's example (IEEE TAC 1999)."""
246
+ _hdr("Optimal digital redesign (Rattan's example)")
247
+
248
+ F = sig.lti([10], [1, 1, 0])
249
+ Kc_ct = sig.lti([0.416, 1], [0.139, 1])
250
+ Q = feedback(mul(F, Kc_ct))
251
+ R = sig.lti([1], [1, 0])
252
+ T = 0.04
253
+
254
+ sys = GeneralizedPlant([
255
+ [mul(Q, R), neg(F)],
256
+ [R, neg(F)],
257
+ ])
258
+
259
+ _sep("L2-optimisation")
260
+ K, err_opt = sdl2(sys, T)
261
+ print(f"Optimal controller: {np.round(K[0],4)} / {np.round(K[1],4)}")
262
+ poles = _cl_poles(F, K, T)
263
+ print(f"Closed-loop poles: {np.round(poles,4)}")
264
+ print(f"Optimal L2 cost: {err_opt:.6f}")
265
+ err = sdl2err(sys, K, T)
266
+ print(f"Direct calculation: {err:.6f}")
267
+
268
+
269
+ # ── demo_cf1 ──────────────────────────────────────────────────────────────────
270
+
271
+ def demo_cf1():
272
+ """Example 12.4.2 from Chen & Francis (1995)."""
273
+ _hdr("Example 12.4.2 – Chen & Francis (1995)")
274
+
275
+ pd = np.convolve([1/12, 1/2, 1], [1/12, 1/2, 1])
276
+ Gh = sig.lti([2], pd)
277
+ Gm = sig.lti([1], [1, 0])
278
+ T = 1
279
+
280
+ sys = GeneralizedPlant([
281
+ [mul(add(Gm, neg(1)), Gh), Gm],
282
+ [neg(mul(Gm, Gh)), neg(Gm)],
283
+ ])
284
+
285
+ _sep("L2-optimisation")
286
+ K, err_L2 = sdl2(sys, T)
287
+ print(f"L2-optimal K: {np.round(K[0],4)} / {np.round(K[1],4)}")
288
+ print(f"L2 cost: {err_L2:.6f}")
289
+ err = sdl2err(sys, K, T)
290
+ print(f"Direct computation: {err:.6f}")
291
+
292
+ _sep("H2-optimisation")
293
+ KH2, err_H2 = sdh2(sys, T)
294
+ print(f"H2-optimal K: {np.round(KH2[0],4)} / {np.round(KH2[1],4)}")
295
+ print(f"H2 cost: {err_H2:.6f}")
296
+ err_L2b = sdl2err(sys, KH2, T)
297
+ print(f"L2 cost: {err_L2b:.6f}")
298
+
299
+
300
+ # ── demo_cf2 ──────────────────────────────────────────────────────────────────
301
+
302
+ def demo_cf2():
303
+ """Examples 6.6.1, 8.4.2, 12.1.1 from Chen & Francis (1995)."""
304
+ _hdr("Examples 6.6.1, 8.4.2, 12.1.1 – Chen & Francis (1995)")
305
+
306
+ F = sig.lti([1], np.convolve([10, 1], [25, 1]))
307
+ R = sig.lti([1], [1, 0])
308
+ T = 1
309
+
310
+ sys = GeneralizedPlant([
311
+ [neg(R), F],
312
+ [R, neg(F)],
313
+ ])
314
+
315
+ _sep("L2-optimisation")
316
+ K, err_opt = sdl2(sys, T)
317
+ print(f"L2-optimal K: {np.round(K[0],4)} / {np.round(K[1],4)}")
318
+ print(f"L2 cost: {err_opt:.6f}")
319
+ err = sdl2err(sys, K, T)
320
+ print(f"Direct computation: {err:.6f}")
321
+
322
+
323
+ # ── demo_dhinf ────────────────────────────────────────────────────────────────
324
+
325
+ def _nd_neg(nd):
326
+ """Negate numerator of a (num, den) tuple."""
327
+ return ([-x for x in nd[0]], nd[1])
328
+
329
+
330
+ def demo_dhinf():
331
+ """Discrete-time polynomial Hinf-optimisation (Grimble 1994)."""
332
+ _hdr("Polynomial Hinf-design for discrete-time systems")
333
+
334
+ T = 1
335
+
336
+ # ── Example 1 (matches MATLAB demo_dhinf example 1) ───────────────────
337
+ _sep("Example 1")
338
+ # F(z) = z/(-2z+1), Fw(z) = (z-2)/(-2z+1)
339
+ # sys = [Fw F; 0 1; -Fw -F] (3×2 generalised plant in z-domain)
340
+ F1 = ([1., 0.], [-2., 1.])
341
+ Fw1 = ([1., -2.], [-2., 1.])
342
+ sys1 = [
343
+ [Fw1, F1],
344
+ [([0.], [1.]), ([1.], [1.])],
345
+ [_nd_neg(Fw1), _nd_neg(F1)],
346
+ ]
347
+ K1, err1 = dhinf(_z2zeta(sys1))
348
+ print(f"Hinf-optimal K: {np.round(K1[0],4)} / {np.round(K1[1],4)}")
349
+ print(f"Optimal Hinf-cost: {err1:.6f}")
350
+ F1_lti = sig.lti(*F1)
351
+ poles1 = _cl_poles(F1_lti, K1, T)
352
+ print(f"Closed-loop poles: {np.round(poles1, 4)}")
353
+
354
+ # ── Example 2 (demo_dhinf.m example 2 — the dsd_help generic example) ──
355
+ # F2 = 1/(z²-2.1z-1), F1 = 2z²+z, Fw = 0.3z+1 (descending-z coefficients
356
+ # of MATLAB's ascending tf([...]) inputs); sys rows: [F2·Fw, F2·F1;
357
+ # 0, 1; -F2·Fw, -F2·F1]. Documented: K = 1.6201(z+0.4019)/(z+1.302),
358
+ # cost 2.1244. (The previous port used an invented z²/(z²-1.6z+0.63)
359
+ # system here — not MATLAB's demo.)
360
+ _sep("Example 2")
361
+ F2d = [-1.0, -2.1, 1.0]
362
+ sys2 = [
363
+ [([0.3, 1.0], F2d), ([2.0, 1.0, 0.0], F2d)],
364
+ [([0.], [1.]), ([1.], [1.])],
365
+ [([-0.3, -1.0], F2d), ([-2.0, -1.0, 0.0], F2d)],
366
+ ]
367
+ K2, err2 = dhinf(_z2zeta(sys2))
368
+ print(f"Hinf-optimal K: {np.round(K2[0],4)} / {np.round(K2[1],4)}")
369
+ print(f"Optimal Hinf-cost: {err2:.6f} (documented 2.1244)")
370
+ print("Documented K: 1.6201 (z+0.4019)/(z+1.302)")
371
+
372
+ # ── Example 3 (demo_dhinf.m example 3 — non-generic case) ──────────────
373
+ # F2 = 1/(0.32-1.2z+z²), F1 = -0.8z+z²... (dsd_help Ex3); two documented
374
+ # equally-optimal controllers, cost 2.5725.
375
+ _sep("Example 3 (non-generic)")
376
+ F3d = [0.32, -1.2, 1.0]
377
+ sys3 = [
378
+ [([-0.4, 1.0], F3d), ([-0.8, 1.0, 0.0], F3d)],
379
+ [([0.], [1.]), ([1.], [1.])],
380
+ [([0.4, -1.0], F3d), ([0.8, -1.0, 0.0], F3d)],
381
+ ]
382
+ K3, err3 = dhinf(_z2zeta(sys3))
383
+ print(f"Optimal Hinf-cost: {err3:.6f} (documented 2.5725)")
384
+ print("Documented optima: -0.72171(z-2.24)(z-0.4)/((z+1.825)(z-0.8859))")
385
+ print(" or 1.9521(z-0.8282)(z-0.4)/(z^2-2.496z+1.617)")
386
+ # Non-generic case: dhinf returns BOTH equally-optimal controllers as a
387
+ # list (MATLAB: K = {K1,K2} cell array).
388
+ for i, Ki in enumerate(K3):
389
+ print(f"K{i}: {np.round(Ki[0],4)} / {np.round(Ki[1],4)}")
390
+ print("\nNote: dhinf solves discrete-time polynomial Hinf for full MIMO plant.")
391
+
392
+
393
+ # ── demo_fil1 ─────────────────────────────────────────────────────────────────
394
+
395
+ def demo_fil1():
396
+ """Optimal sampled-data filtering (Rosenwasser et al., IJACSP 1998)."""
397
+ _hdr("Optimal sampled-data filtering")
398
+
399
+ Sr = sig.lti([4], [-1, 0, 4])
400
+ Fr, _ = sfactor(Sr)
401
+ Fr_lti = to_lti(Fr) # convert ZPK to TF
402
+ F = sig.lti([1], [1, 1])
403
+ Fn = 1
404
+ T = 0.1
405
+
406
+ # 2 rows (z1, y), 3 cols (w1, w2, u)
407
+ sys = GeneralizedPlant([
408
+ [Fr_lti, 0, neg(F)],
409
+ [Fr_lti, Fn, 0],
410
+ ])
411
+
412
+ _sep("H2-optimisation (average variance)")
413
+ K, err_opt = sdh2(sys, T)
414
+ print(f"Optimal H2 cost: {err_opt:.6f}")
415
+ err = sdh2norm(sys, K, T)
416
+ print(f"Average variance: {err:.6f}")
417
+
418
+ _sep("H2-optimisation for t=0 (method='pol')")
419
+ K0, err0 = sdh2(sys, T, t=0.0, method='pol')
420
+ print(f"Variance at t=0: {err0:.6f}")
421
+ err0_v = sdh2norm(sys, K0, T)
422
+ print(f"Average variance: {err0_v:.6f}")
423
+ print("\nNote: inter-sample variance curve requires 'pol' method with t array.")
424
+
425
+
426
+ # ── demo_fil2 ─────────────────────────────────────────────────────────────────
427
+
428
+ def demo_fil2():
429
+ """Optimal sampled-data filtering with time-delay."""
430
+ _hdr("Optimal sampled-data filtering with time-delay")
431
+
432
+ # MATLAB: F.iodelay = 0.051, sys = [Fr 0 -F; Fr Fn 0] (dsd_help.md
433
+ # "Sampled-data filtering with delay"). Fr = sfactor(4/(-s^2+4)) = Fw
434
+ # exactly (see demo_fil1). Delay handled EXACTLY via sdh2's udelay
435
+ # (modified Z-transform, matching MATLAB's F.iodelay semantics) — no
436
+ # Padé in the design plant.
437
+ F = sig.lti([1], [1, 1])
438
+ Fw = sig.lti([2], [1, 2])
439
+ Fn = 1
440
+ T = 0.1
441
+ tau = 0.051
442
+
443
+ sys = GeneralizedPlant([
444
+ [Fw, 0, neg(F)],
445
+ [Fw, Fn, 0],
446
+ ])
447
+
448
+ _sep("H2-optimisation (average variance)")
449
+ K, err_opt = sdh2(sys, T, udelay=tau)
450
+ print(f"H2-optimal K: {np.round(K[0],4)} / {np.round(K[1],4)}")
451
+ print(f"MATLAB K: 3.7544 z(z-0.9048)/((z+0.2678)(z-0.5201))")
452
+ print(f"Optimal H2 cost: {err_opt:.6f}")
453
+
454
+ # sdh2norm has no delay parameter (like sdl2err) — verify on a
455
+ # Padé(3)-approximated plant (fine for evaluation; MATLAB's own
456
+ # documented err/err0 are what a Padé-free exact evaluator would give).
457
+ n_pade = 3
458
+ _c = [math.factorial(2 * n_pade - k) * math.factorial(n_pade)
459
+ / (math.factorial(2 * n_pade) * math.factorial(k) * math.factorial(n_pade - k))
460
+ for k in range(n_pade + 1)]
461
+ pn = np.array([_c[k] * (-tau) ** k for k in range(n_pade + 1)])[::-1]
462
+ pd = np.array([_c[k] * tau ** k for k in range(n_pade + 1)])[::-1]
463
+ F_delay = sig.lti(np.polymul([1], pn), np.polymul([1, 1], pd))
464
+ sys_eval = GeneralizedPlant([
465
+ [Fw, 0, neg(F_delay)],
466
+ [Fw, Fn, 0],
467
+ ])
468
+ err = sdh2norm(sys_eval, K, T)
469
+ print(f"Average variance: {err:.6f} (MATLAB sdh2norm(sys,K)=0.7879)")
470
+
471
+ _sep("H2-optimisation for t=0 (method='pol')")
472
+ # sdh2coef.m's "Instantaneous variance" branch (t != None), combined
473
+ # with the same udelay as the average design above — matches
474
+ # dsd_help.md's documented example exactly (K0 = sdh2(sys,T,0) is
475
+ # called on the SAME delayed `sys`, not a delay-free variant).
476
+ K0, err0 = sdh2(sys, T, t=0.0, udelay=tau)
477
+ print(f"H2-optimal K0: {np.round(K0[0],4)} / {np.round(K0[1],4)}")
478
+ print(f"MATLAB K0: 6.245 z(z-0.9048)/((z+0.9901)(z-0.5201))")
479
+ print(f"Variance at t=0: {err0:.6f} (MATLAB sdh2norm(sys,K0,0)=0.7577)")
480
+ # sdh2norm's t-path now takes udelay directly (exact modified
481
+ # Z-transform, same mechanism as design) instead of forcing a
482
+ # Padé-approximated plant — the Padé route was numerically unusable
483
+ # here since the fast Padé poles interact badly with expm(A*t) at a
484
+ # specific small t.
485
+ err_at0 = sdh2norm(sys, K, T, t=0.0, udelay=tau)
486
+ print(f"K at t=0: {err_at0:.6f} (MATLAB sdh2norm(sys,K,0)=0.7735)")
487
+ err0_avg = sdh2norm(sys_eval, K0, T)
488
+ print(f"K0 average variance: {err0_avg:.6f} (MATLAB sdh2norm(sys,K0)=1.8466)")
489
+
490
+
491
+ # ── demo_fil3 ─────────────────────────────────────────────────────────────────
492
+
493
+ def demo_fil3():
494
+ """Optimal sampled-data filtering for 2nd-order plant."""
495
+ _hdr("Optimal sampled-data filtering – 2nd-order plant")
496
+
497
+ F = sig.lti([1], [1, 1, 1])
498
+ Fw = sig.lti([2], [1, 2])
499
+ Fn = 1
500
+ T = 0.1
501
+
502
+ sys = GeneralizedPlant([
503
+ [neg(Fw), 0, F],
504
+ [Fw, Fn, 0],
505
+ ])
506
+
507
+ _sep("H2-optimisation")
508
+ K, err_opt = sdh2(sys, T)
509
+ print(f"Optimal H2 cost: {err_opt:.6f}")
510
+ err = sdh2norm(sys, K, T)
511
+ print(f"Average variance: {err:.6f}")
512
+
513
+ _sep("H2-optimisation for t=0 (method='pol')")
514
+ K0, err0 = sdh2(sys, T, t=0.0, method='pol')
515
+ print(f"Variance at t=0: {err0:.6f}")
516
+ err0_v = sdh2norm(sys, K0, T)
517
+ print(f"Average variance: {err0_v:.6f}")
518
+
519
+
520
+ # ── demo_hold ─────────────────────────────────────────────────────────────────
521
+
522
+ def demo_hold():
523
+ """Optimal filtering with generalised hold."""
524
+ _hdr("Optimal sampled-data filtering with generalised hold")
525
+
526
+ Sr = sig.lti([4], [-1, 0, 4])
527
+ Fr, _ = sfactor(Sr)
528
+ Fr_lti = to_lti(Fr)
529
+ F = sig.lti([1], [1])
530
+ Fn = 0.1
531
+ T = 0.2
532
+ H = sig.lti([1], [1, 2])
533
+
534
+ sys = GeneralizedPlant([
535
+ [neg(Fr_lti), 0, F],
536
+ [Fr_lti, Fn, 0],
537
+ ])
538
+
539
+ _sep("H2-optimal filter with ZOH")
540
+ K0, err0 = sdh2(sys, T)
541
+ print(f"ZOH H2 cost: {err0:.6f}")
542
+ err0_v = sdh2norm(sys, K0, T)
543
+ print(f"Average variance: {err0_v:.6f}")
544
+
545
+ _sep("H2-optimal filter with generalised hold H=1/(s+2)")
546
+ K, err = sdh2(sys, T, H=H)
547
+ print(f"GH H2 cost: {err:.6f}")
548
+ err_v = sdh2norm(sys, K, T, H=H)
549
+ print(f"Average variance: {err_v:.6f}")
550
+
551
+
552
+ # ── demo_h2hinf ──────────────────────────────────────────────────────────────
553
+
554
+ def demo_h2hinf():
555
+ """Mixed H2/AHinf-optimisation."""
556
+ _hdr("Mixed H2/Hinf optimisation")
557
+
558
+ F = sig.lti([1], [5, 1, 0])
559
+ rho = 1
560
+ T = 1
561
+
562
+ sys = GeneralizedPlant([
563
+ [neg(F), neg(F)],
564
+ [0, rho],
565
+ [neg(F), neg(F)],
566
+ ])
567
+
568
+ _sep("H2-optimisation")
569
+ KH2, err_H2 = sdh2(sys, T)
570
+ print(f"H2-optimal K: {np.round(KH2[0],4)} / {np.round(KH2[1],4)}")
571
+ print(f"H2 cost: {err_H2:.6f}")
572
+ lamH2 = sdahinorm(sys, KH2, T)
573
+ print(f"AHinf cost: {lamH2:.6f}")
574
+ lamH2x = _hinf(F, KH2, T)
575
+ print(f"Hinf norm: {lamH2x:.6f}")
576
+
577
+ _sep("AHinf-optimisation")
578
+ # sdahinf always uses the polynomial (_polhinf) pipeline now, matching
579
+ # MATLAB's sdahinf.m exactly -- for some plants this hits an unresolved
580
+ # _polhinf degeneracy. Report honestly rather than crash.
581
+ try:
582
+ Kinf, lam_inf = sdahinf(sys, T)
583
+ print(f"AHinf-optimal K: {np.round(Kinf[0],4)} / {np.round(Kinf[1],4)}")
584
+ err2 = sdh2norm(sys, Kinf, T)
585
+ print(f"H2 cost: {err2:.6f}")
586
+ print(f"Optimal AHinf-cost: {lam_inf:.6f}")
587
+ lam_d = sdahinorm(sys, Kinf, T)
588
+ print(f"AHinf-cost (verify): {lam_d:.6f}")
589
+ except Exception as exc:
590
+ print(f"AHinf-optimisation failed (_polhinf degeneracy): {exc}")
591
+
592
+ _sep("H2/AHinf-optimisation (rho=0.5)")
593
+ try:
594
+ Kmix, err_mix = sdh2hinf(sys, T, rho=0.5, o11=2, i11=1)
595
+ print(f"Mixed polquad cost: {err_mix:.6f}")
596
+ err_m = sdh2norm(sys, Kmix, T)
597
+ print(f"H2 cost: {err_m:.6f}")
598
+ lam_m = sdahinorm(sys, Kmix, T)
599
+ print(f"AHinf cost: {lam_m:.6f}")
600
+ except Exception as exc:
601
+ print(f"Mixed H2/AHinf-optimisation failed (_polhinf degeneracy): {exc}")
602
+
603
+ _sep("Trade-off: rho sweep")
604
+ print(f" {'rho':>6} {'H2-norm':>10} {'AHinf-norm':>12}")
605
+ for rho_ in [0.0, 0.25, 0.5, 0.75, 1.0]:
606
+ try:
607
+ Km, _ = sdh2hinf(sys, T, rho=rho_, o11=2, i11=1)
608
+ h2 = sdh2norm(sys, Km, T)
609
+ ah = sdahinorm(sys, Km, T)
610
+ print(f" {rho_:>6.2f} {h2:>10.4f} {ah:>12.4f}")
611
+ except Exception as exc:
612
+ print(f" {rho_:>6.2f} failed (_polhinf degeneracy): {exc}")
613
+
614
+
615
+ # ── demo_h2p ──────────────────────────────────────────────────────────────────
616
+
617
+ def demo_h2p():
618
+ """H2-optimal control with preview (Polyakov et al., IEEE AC 2002)."""
619
+ _hdr("H2-optimal controller with preview")
620
+
621
+ F = sig.lti([1], [1, -1]) # 1/(s-1), unstable
622
+ Fr = sig.lti([1], [5, 1])
623
+ Fn = 0.2
624
+ T = 1
625
+ tau = 1.5 # computational delay (MATLAB: F.iodelay = 1.5)
626
+
627
+ sys0 = GeneralizedPlant([
628
+ [Fr, 0, neg(F)],
629
+ [Fr, Fn, neg(F)],
630
+ ])
631
+
632
+ _sep("H2-optimal controller (no preview)")
633
+ K, err_opt = sdh2(sys0, T, udelay=tau)
634
+ print(f"H2 cost (no preview): {err_opt**2:.6f}")
635
+
636
+ _sep("H2-optimal controller (preview pi=2, dsd_help.md documented example)")
637
+ # MATLAB: non-causal preview block removed, equal delay placed in the
638
+ # ideal operator Q instead (Q.iodelay = preview) -- see refdelay's
639
+ # docstring. Since Q=1 (pure delay, no dynamics) here, the delay-free
640
+ # plant above is exactly right; refdelay carries the preview horizon.
641
+ K2, err2 = sdh2(sys0, T, udelay=tau, refdelay=2.0)
642
+ print(f"K: {np.round(K2[0],4)} / {np.round(K2[1],4)}")
643
+ print("MATLAB K: 8.1825 z^2(z-0.7985) / [(z-0.8113)(z^2+3.041z+3.169)]")
644
+ print(f"H2 cost (preview=2): {err2**2:.6f}")
645
+ print("MATLAB sdh2norm(sys,K)^2 = 11.6701")
646
+
647
+ _sep("Dependence on preview horizon pi")
648
+ print(f" {'pi':>6} {'J_min(pi)':>10}")
649
+ for preview in [0.0, 1.0, 2.0, 3.0, 5.0, 7.0, 9.0, 9.999]:
650
+ Kp, errp = sdh2(sys0, T, udelay=tau, refdelay=preview)
651
+ print(f" {preview:>6.2f} {errp**2:>10.4f}")
652
+ print("Note: this plant is UNSTABLE -- dsd_help.md's own documentation")
653
+ print("notes cost is NOT monotonic in pi here (the controller must both")
654
+ print("stabilize the plant and minimize the cost simultaneously); the")
655
+ print("curve dips to a minimum near pi=2 and rises again for larger pi.")
656
+
657
+
658
+ # ── demo_l2 ───────────────────────────────────────────────────────────────────
659
+
660
+ def demo_l2():
661
+ """Design of L2-optimal controller (Polyakov)."""
662
+ _hdr("Sampled-data L2-optimisation")
663
+
664
+ F = sig.lti([1], [5, 1, 0])
665
+ Q = sig.lti([1], [1, 1])
666
+ R = sig.lti([1], [1, 0])
667
+ T = 0.2
668
+
669
+ # 2x2 plant matching MATLAB sys = [Q*R -F; R -F]
670
+ sys = GeneralizedPlant([
671
+ [mul(Q, R), neg(F)],
672
+ [R, neg(F)],
673
+ ])
674
+
675
+ _sep("Sampled-data L2-optimisation")
676
+ K, err_opt = sdl2(sys, T)
677
+ print(f"L2-optimal K: {np.round(K[0],4)} / {np.round(K[1],4)}")
678
+ poles = _cl_poles(F, K, T)
679
+ print(f"Closed-loop poles: {np.round(poles,4)}")
680
+ print(f"Optimal L2 cost: {err_opt:.6f}")
681
+ err = sdl2err(sys, K, T)
682
+ print(f"Direct calculation: {err:.6f}")
683
+
684
+ _sep("Lifting method (sdh2simple + h2reg)")
685
+ # MATLAB: sysH2s = sdh2simple(sys, T); Ks = h2reg(sysH2s) → same K as sdl2.
686
+ dsysL2, *_ = sdh2simple(sys.to_statespace(), T)
687
+ Ks_ss, _ = h2reg(dsysL2)
688
+ Ks_tf = sig.StateSpace(Ks_ss.A, Ks_ss.B, Ks_ss.C, Ks_ss.D, dt=T).to_tf()
689
+ Ks = (np.ravel(Ks_tf.num), np.ravel(Ks_tf.den))
690
+ print(f"Lifting K: {np.round(Ks[0],4)} / {np.round(Ks[1],4)}")
691
+ poles_s = _cl_poles(F, Ks, T)
692
+ print(f"Closed-loop poles: {np.round(poles_s,4)}")
693
+ print(f"Optimal L2 cost: {err_opt:.6f}")
694
+ err_s = sdl2err(sys, Ks, T)
695
+ print(f"Direct calculation: {err_s:.6f}")
696
+
697
+
698
+ # ── demo_l2hinf ───────────────────────────────────────────────────────────────
699
+
700
+ def demo_l2hinf():
701
+ """AHinf-optimisation for tracking system (Polyakov, ARC 2001)."""
702
+ _hdr("AHinf-optimisation for tracking system")
703
+
704
+ F = sig.lti([1], [4, 0.5, 1])
705
+ R = sig.lti([1], [1, 0])
706
+ rho = 0.12
707
+ T = 1
708
+
709
+ sys = GeneralizedPlant([
710
+ [R, neg(F)],
711
+ [mul(rho, R), neg(rho)],
712
+ [R, neg(F)],
713
+ ])
714
+
715
+ _sep("L2-optimisation")
716
+ KL2, err_l2 = sdl2(sys, T)
717
+ print(f"L2-optimal K: {np.round(KL2[0],4)} / {np.round(KL2[1],4)}")
718
+ poles = _cl_poles(F, KL2, T)
719
+ print(f"Closed-loop poles: {np.round(poles,4)}")
720
+ print(f"L2 cost: {err_l2:.6f}")
721
+ lam_l2 = sdtrhinferr(sys, KL2, T)
722
+ print(f"AHinf-cost: {lam_l2:.6f}")
723
+
724
+ _sep("AHinf-optimisation")
725
+ K, lam_opt = sdtrhinf(sys, T)
726
+ print(f"AHinf-optimal K: {np.round(K[0],4)} / {np.round(K[1],4)}")
727
+ poles2 = _cl_poles(F, K, T)
728
+ print(f"Closed-loop poles: {np.round(poles2,4)}")
729
+ err = sdl2err(sys, K, T)
730
+ print(f"L2 cost: {err:.6f}")
731
+ print(f"Optimal AHinf-cost: {lam_opt:.6f}")
732
+ lam1 = sdtrhinferr(sys, K, T)
733
+ print(f"AHinf-cost (verify): {lam1:.6f}")
734
+
735
+
736
+ # ── demo_ait01b ───────────────────────────────────────────────────────────────
737
+
738
+ def demo_ait01b():
739
+ """AHinf-optimisation for tracking system (Polyakov, ARC 2001)."""
740
+ _hdr("AHinf-optimisation for tracking (ARC 2001)")
741
+
742
+ F = sig.lti([1], [1, 1])
743
+ R = sig.lti([1], [1, 0])
744
+ T = 0.2
745
+ Ve, Vu = 1, 0
746
+
747
+ sys = GeneralizedPlant([
748
+ [neg(mul(Ve, R)), mul(Ve, F)],
749
+ [0, Vu],
750
+ [R, neg(F)],
751
+ ])
752
+
753
+ _sep("L2-optimisation")
754
+ KL2, err_l2 = sdl2(sys, T)
755
+ print(f"L2-optimal K: {np.round(KL2[0],4)} / {np.round(KL2[1],4)}")
756
+ poles = _cl_poles(F, KL2, T)
757
+ print(f"Closed-loop poles: {np.round(poles,4)}")
758
+ print(f"L2 cost: {err_l2:.6f}")
759
+ lam = sdtrhinferr(sys, KL2, T)
760
+ print(f"AHinf-cost: {lam:.6f}")
761
+
762
+ _sep("AHinf-optimisation")
763
+ K, lam_opt = sdtrhinf(sys, T)
764
+ print(f"AHinf-optimal K: {np.round(K[0],4)} / {np.round(K[1],4)}")
765
+ poles2 = _cl_poles(F, K, T)
766
+ print(f"Closed-loop poles: {np.round(poles2,4)}")
767
+ err = sdl2err(sys, K, T)
768
+ print(f"L2 cost: {err:.6f}")
769
+ print(f"Optimal AHinf-cost: {lam_opt:.6f}")
770
+ lam1 = sdtrhinferr(sys, K, T)
771
+ print(f"AHinf-cost (verify): {lam1:.6f}")
772
+
773
+
774
+ # ── demo_l2p ──────────────────────────────────────────────────────────────────
775
+
776
+ def demo_l2p():
777
+ """L2-optimal control with preview (Polyakov et al., IFAC-TDS 2003)."""
778
+ _hdr("L2-optimal controller with preview")
779
+
780
+ F = sig.lti([1], [5, -1]) # 1/(5s-1), unstable (exNo=2 in demo_l2p.m)
781
+ Q = sig.lti([1], [0.1, 1])
782
+ R = sig.lti([1], [1, 1, 0])
783
+ T = 1
784
+ tau = 1.5 # computational delay (MATLAB: F.iodelay = 1.5)
785
+
786
+ sys0 = GeneralizedPlant([
787
+ [mul(Q, R), neg(F)],
788
+ [R, neg(F)],
789
+ ])
790
+
791
+ _sep("L2-optimal controller (no preview)")
792
+ K, err_opt = sdl2(sys0, T, udelay=tau)
793
+ poles = _cl_poles(F, K, T)
794
+ print(f"Closed-loop poles: {np.round(poles,4)}")
795
+ print(f"L2 cost (no preview): {err_opt:.6f}")
796
+
797
+ _sep("L2-optimal controller (preview pi=2, dsd_help.md documented example)")
798
+ # MATLAB: non-causal preview block removed, equal delay placed in the
799
+ # ideal operator Q (Q.iodelay=preview) plus a remainder delay theta on
800
+ # the reference generator R (R.iodelay=theta) -- see refdelay's
801
+ # docstring in _sdl2coef for the sigma/theta split. Note this demo's
802
+ # F is the UNSTABLE exNo=2 branch; dsd_help.md's own worked numeric
803
+ # example (sdl2err=0.0517) uses the STABLE F=1/(5s+1) instead -- see
804
+ # ex_l2_preview for that exact validation.
805
+ K2, err2 = sdl2(sys0, T, udelay=tau, refdelay=2.0)
806
+ print(f"K: {np.round(K2[0],4)} / {np.round(K2[1],4)}")
807
+ print(f"L2 cost (preview=2): {err2:.6f}")
808
+
809
+ _sep("Dependence on preview horizon pi")
810
+ print(f" {'pi':>6} {'J_min(pi)':>10}")
811
+ for preview in [0.0, 1.0, 2.0, 3.0, 5.0, 7.0, 9.0, 11.999]:
812
+ Kp, errp = sdl2(sys0, T, udelay=tau, refdelay=preview)
813
+ print(f" {preview:>6.2f} {errp:>10.4f}")
814
+ print("Note: this plant is UNSTABLE -- dsd_help.md's own documentation")
815
+ print("notes cost is NOT monotonic in pi here (the controller must both")
816
+ print("stabilize the plant and minimize the cost simultaneously), unlike")
817
+ print("the stable-plant case where J_min(pi) decreases monotonically.")
818
+
819
+
820
+ # ── demo_2dof ─────────────────────────────────────────────────────────────────
821
+
822
+ def demo_2dof():
823
+ """Optimal 2-DOF tracking system (Polyakov, ARC 2001)."""
824
+ _hdr("Optimal digital 2-DOF controller")
825
+
826
+ F = sig.lti([1], [1, -1])
827
+ R = sig.lti([1], [1, 0])
828
+ Q = sig.lti([1], [1, 2])
829
+ T = 0.5
830
+
831
+ # 1-DOF plant: [z; y] = [Q*R*d - F*u; R*d - F*u]
832
+ sys_1dof = GeneralizedPlant([
833
+ [mul(Q, R), neg(F)],
834
+ [R, neg(F)],
835
+ ])
836
+
837
+ # 2-DOF plant: adds separate reference measurement row y1=R*d, y2=-F*u
838
+ sys_2dof = GeneralizedPlant([
839
+ [mul(Q, R), neg(F)], # performance: Q*R*d - F*u
840
+ [R, 0], # y1: reference channel
841
+ [0, neg(F)], # y2: plant output
842
+ ], n_meas=2)
843
+
844
+ _sep("1-DOF L2-optimisation")
845
+ K, err_1dof = sdl2(sys_1dof, T)
846
+ print(f"1-DOF controller: {np.round(K[0],4)} / {np.round(K[1],4)}")
847
+ poles = _cl_poles(F, K, T)
848
+ print(f"Closed-loop poles: {np.round(poles,4)}")
849
+ print(f"L2 cost: {err_1dof:.6f}")
850
+ err = sdl2err(sys_1dof, K, T)
851
+ print(f"Direct computation: {err:.6f}")
852
+
853
+ _sep("2-DOF feedforward design (sd2dof, feedback K fixed from 1-DOF above)")
854
+ KR, err_2dof = sd2dof(sys_2dof, K, T)
855
+ print(f"2-DOF feedforward KR: {np.round(KR[0],4)} / {np.round(KR[1],4)}")
856
+ print(f"2-DOF L2 cost: {err_2dof:.6f}")
857
+ err2 = sd2doferr(sys_2dof, K, KR, T)
858
+ print(f"L2 cost (verify): {err2:.6f}")
859
+
860
+ _sep("Split 2-DOF: extract KF, KR_new, KC")
861
+ KF, KR_new, KC = split2dof(K, KR)
862
+ print(f"KF (feedback): {np.round(KF[0],4)} / {np.round(KF[1],4)}")
863
+ print(f"KR_new (reference): {np.round(KR_new[0],4)} / {np.round(KR_new[1],4)}")
864
+ print(f"KC (common): {np.round(KC[0],4)} / {np.round(KC[1],4)}")
865
+
866
+
867
+ # ── demo_2dofp ────────────────────────────────────────────────────────────────
868
+
869
+ def demo_2dofp():
870
+ """2-DOF optimal controller with preview (Polyakov et al., CDC 2004)."""
871
+ _hdr("2-DOF optimal controller with preview")
872
+
873
+ F = sig.lti([1], [5, -1])
874
+ Q = sig.lti([1], [0.1, 1])
875
+ R = sig.lti([1], [1, 1, 0])
876
+ T = 1
877
+ tau = 1.5 # computational delay (MATLAB: F.iodelay = 1.5)
878
+
879
+ sys_1dof = GeneralizedPlant([
880
+ [mul(Q, R), neg(F)],
881
+ [R, neg(F)],
882
+ ])
883
+
884
+ sys_2dof = GeneralizedPlant([
885
+ [mul(Q, R), neg(F)], # performance
886
+ [R, 0], # y1: reference channel
887
+ [0, neg(F)], # y2: plant output
888
+ ], n_meas=2)
889
+
890
+ _sep("1-DOF L2 optimisation (preview pi=2, exact via sdl2's refdelay)")
891
+ K, err_1 = sdl2(sys_1dof, T, udelay=tau, refdelay=2.0)
892
+ print(f"1-DOF K: {np.round(K[0],4)} / {np.round(K[1],4)}")
893
+ poles = _cl_poles(F, K, T)
894
+ print(f"Closed-loop poles: {np.round(poles,4)}")
895
+ print(f"L2 cost: {err_1:.6f} (MATLAB sdl2err=2.7072)")
896
+
897
+ _sep("2-DOF feedforward design (sd2dof, feedback K fixed from 1-DOF above)")
898
+ KR, err_2 = sd2dof(sys_2dof, K, T, udelay=tau, refdelay=2.0)
899
+ print(f"2-DOF KR: {np.round(KR[0],4)} / {np.round(KR[1],4)}")
900
+ print(f"2-DOF L2 cost: {err_2:.6f} (MATLAB sd2doferr=0.0602)")
901
+ print("(sd2doferr itself has no delay support yet -- like sdl2err/")
902
+ print("sdh2norm on delayed plants -- so it is not called here; the")
903
+ print("design cost above already matches MATLAB's documented value.)")
904
+
905
+
906
+ # ── demo_modsdh2 ──────────────────────────────────────────────────────────────
907
+
908
+ def demo_modsdh2():
909
+ """Quasioptimal reduced-order H2-controller (ship tanker)."""
910
+ _hdr("Reduced-order H2-optimal controller")
911
+
912
+ F = sig.lti([0.051], [25, 1, 0])
913
+ rho = np.sqrt(0.1)
914
+ Sw = sig.lti([0.0757], [1, 0, 2.489, 0, 1.848])
915
+ Fw, _ = sfactor(Sw)
916
+ Fw_lti = to_lti(Fw)
917
+ T = 1
918
+
919
+ sys = GeneralizedPlant([
920
+ [mul(F, Fw_lti), F],
921
+ [0, rho],
922
+ [neg(mul(F, Fw_lti)), neg(F)],
923
+ ])
924
+
925
+ _sep("Full-order optimal controller")
926
+ KOpt, err_opt = sdh2(sys, T)
927
+ print(f"Full-order K: {np.round(KOpt[0],4)} / {np.round(KOpt[1],4)}")
928
+ print(f"H2-norm: {err_opt:.6f}")
929
+ rOpt = _cl_poles(F, KOpt, T)
930
+ print(f"Closed-loop poles: {np.round(rOpt,4)}")
931
+
932
+ _sep("Reduced-order modal H2 (order 1, SISO P22=F)")
933
+ Kmod, err_mod = modsdh2(F, T, ord_K=1, alpha=0.0, beta=np.inf,
934
+ method='randsearch', n_iter=300)
935
+ print(f"Reduced-order K: {np.round(Kmod[0],4)} / {np.round(Kmod[1],4)}")
936
+ print(f"H2-norm (SISO F): {err_mod:.6f}")
937
+ r_mod = _cl_poles(F, Kmod, T)
938
+ print(f"Closed-loop poles: {np.round(r_mod,4)}")
939
+ print("\nNote: modsdh2/modsdl2 minimise SISO P22 cost (not full MIMO cost).")
940
+
941
+
942
+ # ── demo_modsdh2int ───────────────────────────────────────────────────────────
943
+
944
+ def demo_modsdh2int():
945
+ """Reduced-order H2-optimal controller with an integrator."""
946
+ _hdr("Reduced-order H2-optimal controller with integrator")
947
+
948
+ F1 = sig.lti([0.0694], [18.22, 1])
949
+ F2 = sig.lti([1], [1, 0])
950
+ lam = 0.3; w0 = 0.3; sigma_w = 7.25
951
+ Fw = sig.lti([2*lam*w0*sigma_w, 0], [1, 2*lam*w0, w0**2])
952
+ rho = 2
953
+ T = 2
954
+
955
+ sys = GeneralizedPlant([
956
+ [mul(F2, Fw), mul(F2, F1)],
957
+ [0, rho],
958
+ [neg(mul(F2, Fw)), neg(mul(F2, F1))],
959
+ ])
960
+ F_plant = mul(F2, F1)
961
+
962
+ _sep("Full-order optimal controller")
963
+ KOpt, err_opt = sdh2(sys, T)
964
+ print(f"Full-order K: {np.round(KOpt[0],4)} / {np.round(KOpt[1],4)}")
965
+ print(f"H2-norm: {err_opt:.6f}")
966
+ rOpt = _cl_poles(F_plant, KOpt, T)
967
+ print(f"Closed-loop poles: {np.round(rOpt,4)}")
968
+
969
+ _sep("Reduced-order modal H2 (order 2, SISO P22=F2*F1)")
970
+ Kmod, err_mod = modsdh2(F_plant, T, ord_K=2, alpha=0.02, beta=2,
971
+ method='randsearch', n_iter=300)
972
+ print(f"Reduced-order K: {np.round(Kmod[0],4)} / {np.round(Kmod[1],4)}")
973
+ print(f"H2-norm (SISO): {err_mod:.6f}")
974
+ r_mod = _cl_poles(F_plant, Kmod, T)
975
+ print(f"Closed-loop poles: {np.round(r_mod,4)}")
976
+ alpha_r, beta_r = sector(r_mod, T)
977
+ print(f"Stability sector: α={alpha_r/T:.4f}, β={beta_r:.4f}")
978
+
979
+
980
+ # ── demo_modsdl2 ──────────────────────────────────────────────────────────────
981
+
982
+ def demo_modsdl2():
983
+ """Quasioptimal reduced-order L2-controller."""
984
+ _hdr("Reduced-order L2-optimal controller")
985
+
986
+ F = sig.lti([10], [2, 1, 0])
987
+ R = sig.lti([1], [1, 0])
988
+ Q = sig.lti([1], [1, 2, 1])
989
+ T = 0.2
990
+
991
+ sys = GeneralizedPlant([
992
+ [neg(mul(Q, R)), F],
993
+ [R, neg(F)],
994
+ ])
995
+
996
+ _sep("Full-order optimal controller")
997
+ KOpt, err_opt = sdl2(sys, T)
998
+ print(f"Full-order K: {np.round(KOpt[0],4)} / {np.round(KOpt[1],4)}")
999
+ print(f"L2 cost: {err_opt:.6f}")
1000
+ rOpt = _cl_poles(F, KOpt, T)
1001
+ print(f"Closed-loop poles: {np.round(rOpt,4)}")
1002
+
1003
+ _sep("Reduced-order modal L2 (order 1, SISO plant F)")
1004
+ Kmod, err_mod = modsdl2(F, T, ord_K=1, alpha=0.1, beta=np.inf,
1005
+ method='randsearch', n_iter=300)
1006
+ print(f"Reduced-order K: {np.round(Kmod[0],4)} / {np.round(Kmod[1],4)}")
1007
+ print(f"L2 cost (SISO): {err_mod:.6f}")
1008
+ r_mod = _cl_poles(F, Kmod, T)
1009
+ print(f"Closed-loop poles: {np.round(r_mod,4)}")
1010
+ print("\nNote: modsdl2 cost uses sdl2err on SISO plant F.")
1011
+
1012
+
1013
+ # ── demo_modsdl2a ─────────────────────────────────────────────────────────────
1014
+
1015
+ def demo_modsdl2a():
1016
+ """Quasioptimal reduced-order L2-redesign."""
1017
+ _hdr("L2-optimal redesign – fixed static gain")
1018
+
1019
+ F = sig.lti([1], [1, -1, 0])
1020
+ Kc_ct = sig.lti([5, 1], [1, 3])
1021
+ Q = feedback(mul(F, Kc_ct))
1022
+ R = sig.lti([1], [1, 0])
1023
+ T = 0.5
1024
+
1025
+ sys = GeneralizedPlant([
1026
+ [mul(add(F, neg(Q)), R), F],
1027
+ [neg(mul(F, R)), neg(F)],
1028
+ ])
1029
+
1030
+ _sep("Full-order optimal controller")
1031
+ KOpt, err_opt = sdl2(sys, T)
1032
+ print(f"Full-order K: {np.round(KOpt[0],4)} / {np.round(KOpt[1],4)}")
1033
+ print(f"L2 cost: {err_opt:.6f}")
1034
+ rOpt = _cl_poles(F, KOpt, T)
1035
+ print(f"Closed-loop poles: {np.round(rOpt,4)}")
1036
+
1037
+ _sep("Reduced-order modal L2 (order 2, SISO plant F)")
1038
+ Kmod, err_mod = modsdl2(F, T, ord_K=2, alpha=0.001, beta=np.inf,
1039
+ method='randsearch', n_iter=300)
1040
+ print(f"Reduced-order K: {np.round(Kmod[0],4)} / {np.round(Kmod[1],4)}")
1041
+ print(f"L2 cost (SISO): {err_mod:.6f}")
1042
+ r_mod = _cl_poles(F, Kmod, T)
1043
+ print(f"Closed-loop poles: {np.round(r_mod,4)}")
1044
+
1045
+
1046
+ # ── demo_modsdl2b ─────────────────────────────────────────────────────────────
1047
+
1048
+ def demo_modsdl2b():
1049
+ """Reduced-order L2-optimal controller with fixed static gain."""
1050
+ _hdr("Reduced-order L2-optimal controller – fixed gain")
1051
+
1052
+ F = sig.lti([1], [1, 1, 1])
1053
+ R = sig.lti([1], [1, 0])
1054
+ Q = sig.lti([1], [5, 1])
1055
+ T = 0.2
1056
+
1057
+ sys = GeneralizedPlant([
1058
+ [neg(mul(Q, R)), F],
1059
+ [R, neg(F)],
1060
+ ])
1061
+
1062
+ _sep("Full-order optimal controller")
1063
+ KOpt, err_opt = sdl2(sys, T)
1064
+ print(f"Full-order K: {np.round(KOpt[0],4)} / {np.round(KOpt[1],4)}")
1065
+ print(f"L2 cost: {err_opt:.6f}")
1066
+ rOpt = _cl_poles(F, KOpt, T)
1067
+ print(f"Closed-loop poles: {np.round(rOpt,4)}")
1068
+
1069
+ _sep("Reduced-order modal L2 (order 1, SISO plant F)")
1070
+ Kmod, err_mod = modsdl2(F, T, ord_K=1, alpha=0.0, beta=np.inf,
1071
+ method='randsearch', n_iter=300)
1072
+ print(f"Reduced-order K: {np.round(Kmod[0],4)} / {np.round(Kmod[1],4)}")
1073
+ print(f"L2 cost (SISO): {err_mod:.6f}")
1074
+ r_mod = _cl_poles(F, Kmod, T)
1075
+ print(f"Closed-loop poles: {np.round(r_mod,4)}")
1076
+
1077
+
1078
+ # ── main ──────────────────────────────────────────────────────────────────────
1079
+
1080
+ _ALL_DEMOS = [
1081
+ ("demo_doubint", demo_doubint, "Double integrator – design methods"),
1082
+ ("demo_ait98", demo_ait98, "H2/AHinf comparison (ARC 1998)"),
1083
+ ("demo_at96", demo_at96, "Ship course stabilisation (AT 1996)"),
1084
+ ("demo_autom97", demo_autom97, "H2 with time-delay (Automatica 1997)"),
1085
+ ("demo_c2d", demo_c2d, "Digital redesign (Rattan's example)"),
1086
+ ("demo_cf1", demo_cf1, "Chen & Francis Example 12.4.2"),
1087
+ ("demo_cf2", demo_cf2, "Chen & Francis Examples 6.6.1/8.4.2/12.1.1"),
1088
+ ("demo_dhinf", demo_dhinf, "Discrete-time polynomial Hinf"),
1089
+ ("demo_fil1", demo_fil1, "Optimal filtering (Rosenwasser 1998)"),
1090
+ ("demo_fil2", demo_fil2, "Filtering with time-delay"),
1091
+ ("demo_fil3", demo_fil3, "Filtering – 2nd-order plant"),
1092
+ ("demo_h2hinf", demo_h2hinf, "Mixed H2/AHinf optimisation"),
1093
+ ("demo_h2p", demo_h2p, "H2-optimal control with preview"),
1094
+ ("demo_hold", demo_hold, "Filtering with generalised hold"),
1095
+ ("demo_l2", demo_l2, "L2-optimal controller"),
1096
+ ("demo_l2hinf", demo_l2hinf, "L2/AHinf for tracking (ARC 2001)"),
1097
+ ("demo_ait01b", demo_ait01b, "AHinf tracking (ARC 2001)"),
1098
+ ("demo_l2p", demo_l2p, "L2-optimal with preview"),
1099
+ ("demo_2dof", demo_2dof, "Optimal 2-DOF tracking"),
1100
+ ("demo_2dofp", demo_2dofp, "2-DOF with preview (CDC 2004)"),
1101
+ ("demo_modsdh2", demo_modsdh2, "Reduced-order H2 (modal)"),
1102
+ ("demo_modsdh2int", demo_modsdh2int, "Reduced-order H2 with integrator"),
1103
+ ("demo_modsdl2", demo_modsdl2, "Reduced-order L2 (modal)"),
1104
+ ("demo_modsdl2a", demo_modsdl2a, "L2 redesign"),
1105
+ ("demo_modsdl2b", demo_modsdl2b, "Reduced-order L2 with fixed gain"),
1106
+ ]
1107
+
1108
+
1109
+ def list_demos():
1110
+ """Print a table of available demos."""
1111
+ print("\nAvailable DirectSD demos:")
1112
+ print(f" {'Name':<22} Description")
1113
+ print(f" {'-'*22} {'-'*40}")
1114
+ for name, _, desc in _ALL_DEMOS:
1115
+ print(f" {name:<22} {desc}")
1116
+ print()
1117
+
1118
+
1119
+ def run_demo(name: str) -> None:
1120
+ """Run a single demo by name."""
1121
+ for n, fn, _ in _ALL_DEMOS:
1122
+ if n == name:
1123
+ fn()
1124
+ return
1125
+ raise ValueError(f"Unknown demo '{name}'. Use list_demos() to see available demos.")
1126
+
1127
+
1128
+ def main():
1129
+ """Run all 25 demos sequentially."""
1130
+ import time
1131
+ passed, failed = [], []
1132
+ for name, fn, desc in _ALL_DEMOS:
1133
+ print(f"\n{'─'*60}")
1134
+ print(f"Running {name} …")
1135
+ t0 = time.time()
1136
+ try:
1137
+ fn()
1138
+ elapsed = time.time() - t0
1139
+ passed.append((name, elapsed))
1140
+ print(f"\n[OK] {name} completed in {elapsed:.1f}s")
1141
+ except Exception as exc:
1142
+ import traceback
1143
+ elapsed = time.time() - t0
1144
+ failed.append((name, str(exc)))
1145
+ print(f"\n[FAIL] {name}: {exc}")
1146
+ traceback.print_exc()
1147
+
1148
+ print(f"\n{'='*60}")
1149
+ print(f"Results: {len(passed)} passed, {len(failed)} failed")
1150
+ if failed:
1151
+ print("Failed demos:")
1152
+ for name, msg in failed:
1153
+ print(f" {name}: {msg}")
1154
+
1155
+
1156
+ if __name__ == '__main__':
1157
+ import sys
1158
+ if len(sys.argv) > 1:
1159
+ run_demo(sys.argv[1])
1160
+ else:
1161
+ main()