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,403 @@
|
|
|
1
|
+
"""
|
|
2
|
+
directsd.design.lifting
|
|
3
|
+
=======================
|
|
4
|
+
Lifting (FR-operator) for sampled-data systems.
|
|
5
|
+
|
|
6
|
+
Lifting maps the continuous-time generalized plant P(s) to an
|
|
7
|
+
*exact* discrete-time equivalent P_d that accounts for inter-sample
|
|
8
|
+
behaviour. The H2-optimal discrete controller for P_d is also
|
|
9
|
+
H2-optimal for the original sampled-data loop.
|
|
10
|
+
|
|
11
|
+
References
|
|
12
|
+
----------
|
|
13
|
+
[1] T. Hagiwara & M. Araki, "FR-operator approach to H2-analysis and
|
|
14
|
+
synthesis of sampled-data systems," IEEE TAC, 40(8), 1995.
|
|
15
|
+
[2] T. Chen & B. Francis, "Optimal Sampled-Data Control Systems,"
|
|
16
|
+
Springer, 1995.
|
|
17
|
+
[3] C. F. Van Loan, "Computing integrals involving the matrix
|
|
18
|
+
exponential," IEEE TAC, 23(3), 1978.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
import scipy.linalg as la
|
|
23
|
+
import scipy.signal as sig
|
|
24
|
+
import warnings
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ── helpers ─────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
def _rectchol(M, n_cols=None):
|
|
30
|
+
"""
|
|
31
|
+
Rectangular Cholesky factor: find R (n_cols × size) such that R'R ≈ M.
|
|
32
|
+
Uses an eigendecomposition for positive-semi-definite M.
|
|
33
|
+
When n_cols is None, automatically determines rank via threshold.
|
|
34
|
+
"""
|
|
35
|
+
M = (M + M.T) / 2 # symmetrise
|
|
36
|
+
eigvals, eigvecs = la.eigh(M)
|
|
37
|
+
# Clip tiny negative eigenvalues caused by floating-point rounding
|
|
38
|
+
eigvals = np.maximum(eigvals, 0.0)
|
|
39
|
+
if n_cols is None:
|
|
40
|
+
# Keep only numerically significant eigenvalues (rank-revealing)
|
|
41
|
+
thresh = eigvals.max() * 1e-10 if eigvals.max() > 0 else 0.0
|
|
42
|
+
n_cols = max(1, int(np.sum(eigvals > thresh)))
|
|
43
|
+
# Keep largest n_cols eigenvalues
|
|
44
|
+
idx = np.argsort(eigvals)[::-1][:n_cols]
|
|
45
|
+
R = np.diag(np.sqrt(eigvals[idx])) @ eigvecs[:, idx].T
|
|
46
|
+
return R
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _van_loan_gram(A, B, T):
|
|
50
|
+
"""
|
|
51
|
+
Van Loan's method (1978) to compute the *discrete input Gramian*
|
|
52
|
+
|
|
53
|
+
G_d = ∫_0^T e^{A τ} B B' e^{A' τ} dτ
|
|
54
|
+
|
|
55
|
+
via the matrix exponential of an augmented 2n×2n system:
|
|
56
|
+
|
|
57
|
+
M = expm([−A B B'; 0 A'] * T)
|
|
58
|
+
|
|
59
|
+
G_d = M[n:, n:]' @ M[:n, n:]
|
|
60
|
+
"""
|
|
61
|
+
n = A.shape[0]
|
|
62
|
+
Znn = np.zeros((n, n))
|
|
63
|
+
E = np.block([[-A, B @ B.T],
|
|
64
|
+
[Znn, A.T ]]) * T
|
|
65
|
+
P = la.expm(E)
|
|
66
|
+
Phi_T = P[n:, n:].T # = e^{A T}
|
|
67
|
+
BB_d = Phi_T @ P[:n, n:] # = G_d
|
|
68
|
+
return BB_d, Phi_T
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _van_loan_output(A, B2, C1, D12, T):
|
|
72
|
+
"""
|
|
73
|
+
Van Loan-style computation for the output Gramian integral.
|
|
74
|
+
|
|
75
|
+
Returns (C1d, D12d, Ad, B2d) where C1d is (o1×n) and D12d is (o1×i2).
|
|
76
|
+
"""
|
|
77
|
+
n = A.shape[0]
|
|
78
|
+
i2 = B2.shape[1]
|
|
79
|
+
o1 = C1.shape[0]
|
|
80
|
+
nbar = n + i2
|
|
81
|
+
|
|
82
|
+
Abar = np.block([[A, B2 ],
|
|
83
|
+
[np.zeros((i2, n)), np.zeros((i2, i2)) ]])
|
|
84
|
+
C1D12 = np.hstack([C1, D12]) # (o1 × nbar)
|
|
85
|
+
Q = C1D12.T @ C1D12 # (nbar × nbar) ≥ 0
|
|
86
|
+
|
|
87
|
+
L = np.block([[-Abar.T, Q ],
|
|
88
|
+
[np.zeros((nbar, nbar)), Abar ]]) * T
|
|
89
|
+
M = la.expm(L)
|
|
90
|
+
M22 = M[nbar:, nbar:] # e^{Abar T}
|
|
91
|
+
|
|
92
|
+
CCDD = M22.T @ M[:nbar, nbar:] # nbar × nbar ≥ 0
|
|
93
|
+
|
|
94
|
+
# Rectangular Cholesky factor F such that F'F ≈ CCDD.
|
|
95
|
+
# We need F to be (o1 × nbar) so C1d = F[:, :n] and D12d = F[:, n:].
|
|
96
|
+
# Use eigendecomposition keeping the top o1 modes.
|
|
97
|
+
CCDD = (CCDD + CCDD.T) / 2
|
|
98
|
+
eigvals, eigvecs = la.eigh(CCDD)
|
|
99
|
+
eigvals = np.maximum(eigvals, 0.0)
|
|
100
|
+
|
|
101
|
+
# Keep all numerically significant eigenvalues (rank of CCDD can exceed o1)
|
|
102
|
+
n_keep = np.sum(eigvals > eigvals.max() * 1e-10)
|
|
103
|
+
idx = np.argsort(eigvals)[::-1][:n_keep]
|
|
104
|
+
F = np.diag(np.sqrt(eigvals[idx])) @ eigvecs[:, idx].T # (n_keep × nbar)
|
|
105
|
+
|
|
106
|
+
C1d = F[:, :n] # n_keep × n
|
|
107
|
+
D12d = F[:, n:] # n_keep × i2
|
|
108
|
+
|
|
109
|
+
Ad = M22[:n, :n]
|
|
110
|
+
B2d = M22[:n, n:]
|
|
111
|
+
return C1d, D12d, Ad, B2d
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ── Public API ───────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
def lift_h2(plant_ss, T, n_meas=1, n_ctrl=1):
|
|
117
|
+
"""
|
|
118
|
+
H2-lifting: construct the exact discrete-time H2-equivalent of a
|
|
119
|
+
continuous-time generalized plant (Hagiwara-Araki / Chen-Francis).
|
|
120
|
+
|
|
121
|
+
Parameters
|
|
122
|
+
----------
|
|
123
|
+
plant_ss : scipy.signal.StateSpace
|
|
124
|
+
Continuous-time plant in *standard form*::
|
|
125
|
+
|
|
126
|
+
[z] [P11 P12] [w]
|
|
127
|
+
[y] = [P21 P22] [u]
|
|
128
|
+
|
|
129
|
+
Assumptions (same as MATLAB sdgh2mod):
|
|
130
|
+
• D11 ≈ 0 (no direct disturbance-to-output)
|
|
131
|
+
• D22 ≈ 0 (no direct control-to-measurement)
|
|
132
|
+
T : float
|
|
133
|
+
Sampling period.
|
|
134
|
+
n_meas : int
|
|
135
|
+
Number of measurement outputs (rows of P21/P22).
|
|
136
|
+
n_ctrl : int
|
|
137
|
+
Number of control inputs (cols of P12/P22).
|
|
138
|
+
|
|
139
|
+
Returns
|
|
140
|
+
-------
|
|
141
|
+
dsys : scipy.signal.StateSpace
|
|
142
|
+
Exact discrete-time H2-equivalent (without Δ correction term).
|
|
143
|
+
gamma : float
|
|
144
|
+
Additional H2-cost term due to continuous intersample signal energy
|
|
145
|
+
(from B1 channel). Full cost: J = ‖dsys‖²_H2 + gamma.
|
|
146
|
+
dsys_delta : scipy.signal.StateSpace
|
|
147
|
+
Variant that includes the Δ feedthrough block (FR-operator form).
|
|
148
|
+
|
|
149
|
+
Notes
|
|
150
|
+
-----
|
|
151
|
+
The user workflow is::
|
|
152
|
+
|
|
153
|
+
from directsd.design.lifting import lift_h2
|
|
154
|
+
from directsd import h2reg
|
|
155
|
+
|
|
156
|
+
plant_lifted, gamma, _ = lift_h2(plant_ss, T)
|
|
157
|
+
K, h2n = h2reg(plant_lifted, n_meas=1, n_ctrl=1)
|
|
158
|
+
total_cost = np.sqrt(h2n**2 + gamma)
|
|
159
|
+
"""
|
|
160
|
+
if not isinstance(plant_ss, sig.StateSpace):
|
|
161
|
+
raise TypeError("plant_ss must be a scipy.signal.StateSpace")
|
|
162
|
+
if plant_ss.dt not in (None, 0):
|
|
163
|
+
raise ValueError("plant_ss must be a continuous-time system (dt=None or 0)")
|
|
164
|
+
|
|
165
|
+
A = plant_ss.A
|
|
166
|
+
B = plant_ss.B
|
|
167
|
+
C = plant_ss.C
|
|
168
|
+
D = plant_ss.D
|
|
169
|
+
n = A.shape[0]
|
|
170
|
+
|
|
171
|
+
nout, nin = C.shape[0], B.shape[1]
|
|
172
|
+
i1 = nin - n_ctrl
|
|
173
|
+
o1 = nout - n_meas
|
|
174
|
+
|
|
175
|
+
if i1 < 1:
|
|
176
|
+
raise ValueError("No disturbance inputs (i1 < 1)")
|
|
177
|
+
if o1 < 1:
|
|
178
|
+
raise ValueError("No performance outputs (o1 < 1)")
|
|
179
|
+
|
|
180
|
+
B1 = B[:, :i1]; B2 = B[:, i1:]
|
|
181
|
+
C1 = C[:o1, :]; C2 = C[o1:, :]
|
|
182
|
+
D11 = D[:o1, :i1]; D12 = D[:o1, i1:]
|
|
183
|
+
D21 = D[o1:, :i1]; D22 = D[o1:, i1:]
|
|
184
|
+
|
|
185
|
+
if np.linalg.norm(D11) > 1e-8 * (np.linalg.norm(D) + 1):
|
|
186
|
+
warnings.warn("D11 ≠ 0 – H2-lifting assumes D11 = 0")
|
|
187
|
+
if np.linalg.norm(D22) > 1e-8 * (np.linalg.norm(D) + 1):
|
|
188
|
+
warnings.warn("D22 ≠ 0 – H2-lifting assumes D22 = 0")
|
|
189
|
+
|
|
190
|
+
# ── Discretise B1 channel via Van Loan (input Gramian) ──────────────────
|
|
191
|
+
BB1d, Phi_T = _van_loan_gram(A, B1, T)
|
|
192
|
+
# Rectangular Cholesky: B1d B1d' = BB1d
|
|
193
|
+
B1d = _rectchol(BB1d).T # n × i1d (i1d ≥ i1)
|
|
194
|
+
i1d = B1d.shape[1]
|
|
195
|
+
|
|
196
|
+
# ── Discretise B2/C1 channel (output Gramian) ───────────────────────────
|
|
197
|
+
C1d, D12d, Ad, B2d = _van_loan_output(A, B2, C1, D12, T)
|
|
198
|
+
|
|
199
|
+
# Scale by 1/√T (H2-norm convention)
|
|
200
|
+
sqrtT = np.sqrt(T)
|
|
201
|
+
C1d = C1d / sqrtT
|
|
202
|
+
D12d = D12d / sqrtT
|
|
203
|
+
|
|
204
|
+
# ── Delta feedthrough (FR-operator correction) ──────────────────────────
|
|
205
|
+
# Δ satisfies: B1d @ Δ' @ [C1d D12d] = Q_correction
|
|
206
|
+
# Computed via pseudo-inverse (cf. frdelta in sdgh2mod.m)
|
|
207
|
+
nbar = n + n_ctrl
|
|
208
|
+
ah1 = np.block([[A, B1 @ B1.T ],
|
|
209
|
+
[np.zeros((n, n)), -A.T ]])
|
|
210
|
+
ah2 = np.block([[A, B2 ],
|
|
211
|
+
[np.zeros((n_ctrl, n + n_ctrl)) ]])
|
|
212
|
+
bh = np.block([[np.zeros((n, n + n_ctrl)) ],
|
|
213
|
+
[-C1.T @ C1, -C1.T @ D12 ]])
|
|
214
|
+
H_big = np.block([[ah1, bh ],
|
|
215
|
+
[np.zeros((n + n_ctrl, 2*n)), ah2 ]])
|
|
216
|
+
expH = la.expm(H_big * T)
|
|
217
|
+
ind_n = slice(n, 2*n)
|
|
218
|
+
PhiInvT = expH[ind_n, ind_n]
|
|
219
|
+
q_corr = (expH[:n, 2*n:]
|
|
220
|
+
+ BB1d @ PhiInvT @ C1d.T @ np.hstack([C1d, D12d]))
|
|
221
|
+
# Δ = pinv(B1d) @ q_corr @ pinv([C1d D12d]')
|
|
222
|
+
C1dD12d = np.hstack([C1d, D12d])
|
|
223
|
+
Delta = (np.linalg.pinv(B1d) @ q_corr @ np.linalg.pinv(C1dD12d.T).T).T
|
|
224
|
+
Delta = Delta / sqrtT
|
|
225
|
+
|
|
226
|
+
# ── Assemble discrete systems ────────────────────────────────────────────
|
|
227
|
+
Bd = np.hstack([B1d, B2d])
|
|
228
|
+
Cd = np.vstack([C1d, C2])
|
|
229
|
+
|
|
230
|
+
# C1d is (o1_d × n), D12d is (o1_d × n_ctrl). o1_d may equal o1.
|
|
231
|
+
o1_d = C1d.shape[0]
|
|
232
|
+
|
|
233
|
+
Dd_plain = np.block([[np.zeros((o1_d, i1d)), D12d ],
|
|
234
|
+
[np.zeros((n_meas, i1d + n_ctrl)) ]])
|
|
235
|
+
Dd_delta = np.block([[Delta, D12d ],
|
|
236
|
+
[np.zeros((n_meas, Delta.shape[1] + n_ctrl)) ]])
|
|
237
|
+
|
|
238
|
+
# Handle non-zero D21
|
|
239
|
+
if np.linalg.norm(D21) > 1e-10:
|
|
240
|
+
Bd = np.hstack([B1d, np.zeros((n, i1)), B2d])
|
|
241
|
+
Dd_plain = np.block([[np.zeros((o1_d, i1d)), np.zeros((o1_d, i1)), D12d],
|
|
242
|
+
[np.zeros((n_meas, i1d)), D21, np.zeros((n_meas, n_ctrl))]])
|
|
243
|
+
Dd_delta = np.block([[Delta, np.zeros((o1_d, i1)), D12d],
|
|
244
|
+
[np.zeros((n_meas, Delta.shape[1])), D21, np.zeros((n_meas, n_ctrl))]])
|
|
245
|
+
|
|
246
|
+
dsys = sig.StateSpace(Ad, Bd, Cd, Dd_plain, dt=T)
|
|
247
|
+
dsys_delta = sig.StateSpace(Ad, Bd, Cd, Dd_delta, dt=T)
|
|
248
|
+
|
|
249
|
+
# ── Additional H2-cost term γ (inter-sample energy of B1 channel) ───────
|
|
250
|
+
# γ = (1/T) tr( B1' ∫ e^{A'τ} C1'C1 e^{Aτ} dτ B1 )
|
|
251
|
+
H_gamma = np.block([[-A.T, np.eye(n), np.zeros((n, n))],
|
|
252
|
+
[np.zeros((n,n)), -A.T, C1.T @ C1 ],
|
|
253
|
+
[np.zeros((n,n)), np.zeros((n,n)), A ]])
|
|
254
|
+
expHg = la.expm(H_gamma * T)
|
|
255
|
+
gamma = float(np.real(np.trace(
|
|
256
|
+
B1.T @ expHg[2*n:, 2*n:].T @ expHg[:n, 2*n:] @ B1
|
|
257
|
+
)) / T)
|
|
258
|
+
|
|
259
|
+
return dsys, gamma, dsys_delta
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def lift_l2(plant_ss, T, n_meas=1, n_ctrl=1):
|
|
263
|
+
"""
|
|
264
|
+
L2-lifting: simplified H2-equivalent (Chen-Francis, 1995).
|
|
265
|
+
|
|
266
|
+
Same as lift_h2 but omits the Δ correction (assumes D11 = D21 = D22 = 0
|
|
267
|
+
strictly). Faster and numerically cleaner for L2-optimal design.
|
|
268
|
+
|
|
269
|
+
Parameters
|
|
270
|
+
----------
|
|
271
|
+
plant_ss, T, n_meas, n_ctrl : same as lift_h2.
|
|
272
|
+
|
|
273
|
+
Returns
|
|
274
|
+
-------
|
|
275
|
+
dsys : scipy.signal.StateSpace
|
|
276
|
+
Exact discrete-time L2-equivalent.
|
|
277
|
+
"""
|
|
278
|
+
if not isinstance(plant_ss, sig.StateSpace):
|
|
279
|
+
raise TypeError("plant_ss must be a scipy.signal.StateSpace")
|
|
280
|
+
|
|
281
|
+
A = plant_ss.A; B = plant_ss.B
|
|
282
|
+
C = plant_ss.C; D = plant_ss.D
|
|
283
|
+
n = A.shape[0]
|
|
284
|
+
|
|
285
|
+
nout, nin = C.shape[0], B.shape[1]
|
|
286
|
+
i1 = nin - n_ctrl; o1 = nout - n_meas
|
|
287
|
+
B1 = B[:, :i1]; B2 = B[:, i1:]
|
|
288
|
+
C1 = C[:o1, :]; C2 = C[o1:, :]
|
|
289
|
+
D12 = D[:o1, i1:]
|
|
290
|
+
|
|
291
|
+
# Output channel (loop + output discretisation)
|
|
292
|
+
C1d, D12d, Ad, B2d = _van_loan_output(A, B2, C1, D12, T)
|
|
293
|
+
sqrtT = np.sqrt(T)
|
|
294
|
+
C1d = C1d / sqrtT
|
|
295
|
+
D12d = D12d / sqrtT
|
|
296
|
+
|
|
297
|
+
# Input channel: B1 is passed through as continuous (no lifting needed
|
|
298
|
+
# for L2 – impulse disturbances)
|
|
299
|
+
i1d = i1
|
|
300
|
+
B1d = B1
|
|
301
|
+
|
|
302
|
+
o1_d = C1d.shape[0]
|
|
303
|
+
Bd = np.hstack([B1d, B2d])
|
|
304
|
+
Cd = np.vstack([C1d, C2])
|
|
305
|
+
Dd = np.block([[np.zeros((o1_d, i1d)), D12d ],
|
|
306
|
+
[np.zeros((n_meas, i1d + n_ctrl)) ]])
|
|
307
|
+
|
|
308
|
+
return sig.StateSpace(Ad, Bd, Cd, Dd, dt=T)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def lft_dt(plant_ss_dt, K_ss_dt, n_meas=1, n_ctrl=1):
|
|
312
|
+
"""
|
|
313
|
+
Lower linear fractional transformation: close the sampled-data loop.
|
|
314
|
+
|
|
315
|
+
Returns the DT closed-loop StateSpace from exogenous input w to
|
|
316
|
+
performance output z::
|
|
317
|
+
|
|
318
|
+
Fl(P, K) = P11 + P12 * K * (I - P22*K)^{-1} * P21
|
|
319
|
+
|
|
320
|
+
Parameters
|
|
321
|
+
----------
|
|
322
|
+
plant_ss_dt : scipy.signal.StateSpace (discrete, dt=T)
|
|
323
|
+
Lifted DT plant with block structure::
|
|
324
|
+
|
|
325
|
+
[z] [P11 P12] [w]
|
|
326
|
+
[y] = [P21 P22] [u]
|
|
327
|
+
|
|
328
|
+
K_ss_dt : scipy.signal.StateSpace (discrete, dt=T)
|
|
329
|
+
Discrete-time controller K(z).
|
|
330
|
+
n_meas : int
|
|
331
|
+
Number of measurement outputs (rows of P21/P22).
|
|
332
|
+
n_ctrl : int
|
|
333
|
+
Number of control inputs (cols of P12/P22).
|
|
334
|
+
|
|
335
|
+
Returns
|
|
336
|
+
-------
|
|
337
|
+
dcl : scipy.signal.StateSpace
|
|
338
|
+
Closed-loop system (w → z).
|
|
339
|
+
"""
|
|
340
|
+
Ap = plant_ss_dt.A; Bp = plant_ss_dt.B
|
|
341
|
+
Cp = plant_ss_dt.C; Dp = plant_ss_dt.D
|
|
342
|
+
T = plant_ss_dt.dt
|
|
343
|
+
|
|
344
|
+
nout = Cp.shape[0]; nin = Bp.shape[1]
|
|
345
|
+
o1 = nout - n_meas; i1 = nin - n_ctrl
|
|
346
|
+
|
|
347
|
+
B1 = Bp[:, :i1]; B2 = Bp[:, i1:]
|
|
348
|
+
C1 = Cp[:o1, :]; C2 = Cp[o1:, :]
|
|
349
|
+
D11 = Dp[:o1, :i1]; D12 = Dp[:o1, i1:]
|
|
350
|
+
D21 = Dp[o1:, :i1]; D22 = Dp[o1:, i1:]
|
|
351
|
+
|
|
352
|
+
Ak = K_ss_dt.A; Bk = K_ss_dt.B
|
|
353
|
+
Ck = K_ss_dt.C; Dk = K_ss_dt.D
|
|
354
|
+
|
|
355
|
+
# M1 = (I - Dk @ D22)^{-1}, M2 = (I - D22 @ Dk)^{-1}
|
|
356
|
+
I_k = np.eye(n_ctrl)
|
|
357
|
+
I_m = np.eye(n_meas)
|
|
358
|
+
try:
|
|
359
|
+
M1 = np.linalg.solve(I_k - Dk @ D22, I_k)
|
|
360
|
+
except np.linalg.LinAlgError:
|
|
361
|
+
M1 = I_k # fallback when D22 = 0
|
|
362
|
+
try:
|
|
363
|
+
M2 = np.linalg.solve(I_m - D22 @ Dk, I_m)
|
|
364
|
+
except np.linalg.LinAlgError:
|
|
365
|
+
M2 = I_m
|
|
366
|
+
|
|
367
|
+
Acl = np.block([
|
|
368
|
+
[Ap + B2 @ Dk @ M2 @ C2, B2 @ M1 @ Ck ],
|
|
369
|
+
[Bk @ M2 @ C2, Ak + Bk @ M2 @ D22 @ Ck ],
|
|
370
|
+
])
|
|
371
|
+
Bcl = np.vstack([
|
|
372
|
+
B1 + B2 @ Dk @ M2 @ D21,
|
|
373
|
+
Bk @ M2 @ D21,
|
|
374
|
+
])
|
|
375
|
+
Ccl = np.hstack([
|
|
376
|
+
C1 + D12 @ Dk @ M2 @ C2,
|
|
377
|
+
D12 @ M1 @ Ck,
|
|
378
|
+
])
|
|
379
|
+
Dcl = D11 + D12 @ Dk @ M2 @ D21
|
|
380
|
+
|
|
381
|
+
return sig.StateSpace(Acl, Bcl, Ccl, Dcl, dt=T)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def compute_gamma(plant_ss, T, n_meas=1, n_ctrl=1):
|
|
385
|
+
"""
|
|
386
|
+
Compute the inter-sample correction term γ for H2-optimal design.
|
|
387
|
+
|
|
388
|
+
γ = (1/T) tr( B1' * ∫_0^T e^{A'τ} C1'C1 e^{Aτ} dτ * B1 )
|
|
389
|
+
|
|
390
|
+
The total H2-cost is J = √(‖P_lifted‖²_H2 + γ).
|
|
391
|
+
|
|
392
|
+
Parameters
|
|
393
|
+
----------
|
|
394
|
+
plant_ss : scipy.signal.StateSpace
|
|
395
|
+
T : float
|
|
396
|
+
n_meas, n_ctrl : int
|
|
397
|
+
|
|
398
|
+
Returns
|
|
399
|
+
-------
|
|
400
|
+
gamma : float
|
|
401
|
+
"""
|
|
402
|
+
_, gamma, _ = lift_h2(plant_ss, T, n_meas, n_ctrl)
|
|
403
|
+
return gamma
|