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,449 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Miscellaneous utility functions for DirectSD.
|
|
3
|
+
|
|
4
|
+
Ports of: bilintr (bilinear transformation), improper (proper/improper split),
|
|
5
|
+
sumzpk (reliable zpk sum), separtf (separation via polynomial
|
|
6
|
+
equations), tf2nd (TF num/den extract)
|
|
7
|
+
|
|
8
|
+
Diophantine solvers (dioph, dioph2, diophsys, diophsys2) live in
|
|
9
|
+
directsd.polynomial.diophantine. A second, incorrect `diophsys` used to live
|
|
10
|
+
here (single shared Y for both equations, `[X,Y,err]`) -- it had zero callers
|
|
11
|
+
anywhere in the codebase and didn't match MATLAB's diophsys.m (which returns
|
|
12
|
+
separate Y1/Y2, `[X,Y1,Y2,err,condA]`) despite being the one exported as the
|
|
13
|
+
public `directsd.diophsys`. It has been removed.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# bilintr – Bilinear transformation for SISO transfer functions
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
def bilintr(sys, btype=None, T=1.0):
|
|
24
|
+
"""
|
|
25
|
+
Bilinear transformation for a SISO LTI system.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
sys : (num, den) tuple or scipy lti/dlti
|
|
30
|
+
Input system.
|
|
31
|
+
btype : str, optional
|
|
32
|
+
Transformation type:
|
|
33
|
+
's2z' - s → z, maps LHP into unit disk
|
|
34
|
+
'z2s' - z → s, maps unit disk into LHP (default for discrete sys)
|
|
35
|
+
's2d' - s → d=1/z, maps RHP into unit disk
|
|
36
|
+
'd2s' - d → s
|
|
37
|
+
'tustin' - Tustin (bilinear) transform with sampling period T
|
|
38
|
+
[a,b,c,d] - custom coefficients y = (a*x+b)/(c*x+d)
|
|
39
|
+
T : float
|
|
40
|
+
Sampling period (used for 'tustin').
|
|
41
|
+
|
|
42
|
+
Returns
|
|
43
|
+
-------
|
|
44
|
+
sys_out : (num, den) tuple
|
|
45
|
+
Transformed system.
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
import scipy.signal as sig
|
|
49
|
+
except ImportError:
|
|
50
|
+
raise ImportError("scipy is required.")
|
|
51
|
+
|
|
52
|
+
# Extract num/den
|
|
53
|
+
if isinstance(sys, sig.lti):
|
|
54
|
+
num, den = sys.num, sys.den
|
|
55
|
+
is_ct = True
|
|
56
|
+
elif isinstance(sys, sig.dlti):
|
|
57
|
+
num, den = sys.num, sys.den
|
|
58
|
+
is_ct = False
|
|
59
|
+
elif isinstance(sys, tuple) and len(sys) == 2:
|
|
60
|
+
num, den = sys
|
|
61
|
+
is_ct = True
|
|
62
|
+
else:
|
|
63
|
+
raise TypeError(f"Unsupported system type {type(sys)}")
|
|
64
|
+
|
|
65
|
+
num = np.atleast_1d(np.array(num, dtype=float)).ravel()
|
|
66
|
+
den = np.atleast_1d(np.array(den, dtype=float)).ravel()
|
|
67
|
+
|
|
68
|
+
# Determine default type
|
|
69
|
+
if btype is None:
|
|
70
|
+
btype = 'z2s' if not is_ct else 's2z'
|
|
71
|
+
|
|
72
|
+
# Handle custom coefficients [a, b, c, d]
|
|
73
|
+
if isinstance(btype, (list, np.ndarray)):
|
|
74
|
+
coefs = np.array(btype, dtype=float).ravel()
|
|
75
|
+
if len(coefs) == 1:
|
|
76
|
+
b_val = coefs[0]
|
|
77
|
+
a, b, c, d = 1.0, b_val, 1.0, -b_val
|
|
78
|
+
elif len(coefs) == 4:
|
|
79
|
+
a, b, c, d = coefs
|
|
80
|
+
else:
|
|
81
|
+
raise ValueError("Custom bilinear coefficients must be length 1 or 4")
|
|
82
|
+
else:
|
|
83
|
+
btype = btype.lower()
|
|
84
|
+
if btype == 's2z':
|
|
85
|
+
a, b, c, d = 1.0, 1.0, -1.0, 1.0 # z = (s+1)/(-s+1)
|
|
86
|
+
elif btype == 'z2s':
|
|
87
|
+
a, b, c, d = 1.0, -1.0, 1.0, 1.0 # s = (z-1)/(z+1)
|
|
88
|
+
elif btype == 's2d':
|
|
89
|
+
a, b, c, d = -1.0, 1.0, 1.0, 1.0 # d = (-s+1)/(s+1)
|
|
90
|
+
elif btype == 'd2s':
|
|
91
|
+
a, b, c, d = -1.0, 1.0, 1.0, 1.0 # s = (-d+1)/(d+1)
|
|
92
|
+
elif btype == 'tustin':
|
|
93
|
+
# z = (1 + s*T/2) / (1 - s*T/2) => s = 2/T * (z-1)/(z+1)
|
|
94
|
+
a, b, c, d = 2.0/T, -2.0/T, 1.0, 1.0
|
|
95
|
+
else:
|
|
96
|
+
raise ValueError(f"Unknown bilinear type '{btype}'")
|
|
97
|
+
|
|
98
|
+
# Apply substitution x = (a*w + b) / (c*w + d) where w is new variable
|
|
99
|
+
# F(x) = num(x)/den(x); substitute x = (a*w+b)/(c*w+d)
|
|
100
|
+
n = max(len(num), len(den)) - 1
|
|
101
|
+
|
|
102
|
+
# Compute numerator and denominator polynomials in w
|
|
103
|
+
# by substituting the rational expression
|
|
104
|
+
# num_out(w) = num((aw+b)/(cw+d)) * (cw+d)^n
|
|
105
|
+
# den_out(w) = den((aw+b)/(cw+d)) * (cw+d)^n
|
|
106
|
+
# Build [aw+b]^k and [cw+d]^(n-k) for each coefficient
|
|
107
|
+
|
|
108
|
+
def poly_power(coeffs, power):
|
|
109
|
+
"""Raise polynomial [a,b] to `power`."""
|
|
110
|
+
result = np.array([1.0])
|
|
111
|
+
base = np.array(coeffs, dtype=float)
|
|
112
|
+
for _ in range(int(power)):
|
|
113
|
+
result = np.polymul(result, base)
|
|
114
|
+
return result
|
|
115
|
+
|
|
116
|
+
ab = np.array([a, b])
|
|
117
|
+
cd = np.array([c, d])
|
|
118
|
+
|
|
119
|
+
num_w = np.array([0.0])
|
|
120
|
+
for i, coef in enumerate(num):
|
|
121
|
+
pwr = len(num) - 1 - i
|
|
122
|
+
term = coef * np.polymul(poly_power(ab, pwr), poly_power(cd, n - pwr))
|
|
123
|
+
num_w = np.polyadd(num_w, term)
|
|
124
|
+
|
|
125
|
+
den_w = np.array([0.0])
|
|
126
|
+
for i, coef in enumerate(den):
|
|
127
|
+
pwr = len(den) - 1 - i
|
|
128
|
+
term = coef * np.polymul(poly_power(ab, pwr), poly_power(cd, n - pwr))
|
|
129
|
+
den_w = np.polyadd(den_w, term)
|
|
130
|
+
|
|
131
|
+
# Normalize
|
|
132
|
+
num_w = np.real_if_close(num_w, tol=1e6)
|
|
133
|
+
den_w = np.real_if_close(den_w, tol=1e6)
|
|
134
|
+
|
|
135
|
+
# Strip leading zeros
|
|
136
|
+
from directsd.polynomial.operations import striplz
|
|
137
|
+
num_w = striplz(num_w)
|
|
138
|
+
den_w = striplz(den_w)
|
|
139
|
+
|
|
140
|
+
# Normalize leading coefficient of denominator to 1
|
|
141
|
+
lead = den_w[0]
|
|
142
|
+
if abs(lead) > 1e-14:
|
|
143
|
+
num_w = num_w / lead
|
|
144
|
+
den_w = den_w / lead
|
|
145
|
+
|
|
146
|
+
return num_w, den_w
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ---------------------------------------------------------------------------
|
|
150
|
+
# improper – separate improper (polynomial) part of rational function
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
def improper(sys, ptype='sp'):
|
|
154
|
+
"""
|
|
155
|
+
Separate the improper (polynomial) part of a rational transfer function.
|
|
156
|
+
|
|
157
|
+
Parameters
|
|
158
|
+
----------
|
|
159
|
+
sys : (num, den) tuple or scipy lti
|
|
160
|
+
Input rational function.
|
|
161
|
+
ptype : str
|
|
162
|
+
'sp' - strictly proper remainder (default)
|
|
163
|
+
'p' - proper remainder (may have direct term)
|
|
164
|
+
'symm' - same as 'sp' (for symmetric spectral densities)
|
|
165
|
+
|
|
166
|
+
Returns
|
|
167
|
+
-------
|
|
168
|
+
P : np.ndarray
|
|
169
|
+
Polynomial (improper) part coefficients.
|
|
170
|
+
R0 : (num, den) tuple
|
|
171
|
+
Proper or strictly proper part.
|
|
172
|
+
"""
|
|
173
|
+
try:
|
|
174
|
+
import scipy.signal as sig
|
|
175
|
+
except ImportError:
|
|
176
|
+
raise ImportError("scipy is required.")
|
|
177
|
+
|
|
178
|
+
if ptype not in ('sp', 'p', 'symm'):
|
|
179
|
+
raise ValueError(f"Unknown properness type '{ptype}'")
|
|
180
|
+
|
|
181
|
+
if isinstance(sys, sig.lti):
|
|
182
|
+
num, den = sys.num, sys.den
|
|
183
|
+
elif isinstance(sys, tuple):
|
|
184
|
+
num, den = sys
|
|
185
|
+
else:
|
|
186
|
+
raise TypeError(f"Unsupported type {type(sys)}")
|
|
187
|
+
|
|
188
|
+
num = np.atleast_1d(np.array(num, dtype=float)).ravel()
|
|
189
|
+
den = np.atleast_1d(np.array(den, dtype=float)).ravel()
|
|
190
|
+
|
|
191
|
+
if len(num) <= len(den):
|
|
192
|
+
# Already proper or strictly proper
|
|
193
|
+
if ptype == 'p':
|
|
194
|
+
return np.array([0.0]), (num, den)
|
|
195
|
+
else:
|
|
196
|
+
# Check for direct term
|
|
197
|
+
if len(num) == len(den):
|
|
198
|
+
d = num[0] / den[0]
|
|
199
|
+
num_sp = num - d * den
|
|
200
|
+
from directsd.polynomial.operations import striplz
|
|
201
|
+
num_sp = striplz(num_sp)
|
|
202
|
+
return np.array([d]), (num_sp, den)
|
|
203
|
+
return np.array([0.0]), (num, den)
|
|
204
|
+
|
|
205
|
+
# Polynomial long division: num = P * den + R
|
|
206
|
+
P, R = np.polydiv(num, den)
|
|
207
|
+
|
|
208
|
+
from directsd.polynomial.operations import striplz
|
|
209
|
+
P = striplz(P) if len(P) > 0 else np.array([0.0])
|
|
210
|
+
R = striplz(R) if len(R) > 0 else np.array([0.0])
|
|
211
|
+
|
|
212
|
+
if ptype == 'p':
|
|
213
|
+
# Remainder may still have direct term: split it off
|
|
214
|
+
if len(R) == len(den):
|
|
215
|
+
d = R[0] / den[0]
|
|
216
|
+
R = R - d * den
|
|
217
|
+
R = striplz(R)
|
|
218
|
+
P = np.polyadd(P, np.array([d]))
|
|
219
|
+
# for 'sp' and 'symm': P includes all polynomial terms including constant
|
|
220
|
+
|
|
221
|
+
return P, (R, den)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# ---------------------------------------------------------------------------
|
|
225
|
+
# tf2nd – extract numerator/denominator from transfer function
|
|
226
|
+
# ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
def tf2nd(sys):
|
|
229
|
+
"""
|
|
230
|
+
Extract numerator and denominator coefficient arrays from a TF.
|
|
231
|
+
|
|
232
|
+
Parameters
|
|
233
|
+
----------
|
|
234
|
+
sys : (num, den) tuple or scipy lti/dlti
|
|
235
|
+
|
|
236
|
+
Returns
|
|
237
|
+
-------
|
|
238
|
+
num : np.ndarray
|
|
239
|
+
den : np.ndarray
|
|
240
|
+
"""
|
|
241
|
+
try:
|
|
242
|
+
import scipy.signal as sig
|
|
243
|
+
except ImportError:
|
|
244
|
+
raise ImportError("scipy is required.")
|
|
245
|
+
|
|
246
|
+
if isinstance(sys, (sig.lti, sig.dlti)):
|
|
247
|
+
tf = sys if isinstance(sys, sig.TransferFunction) else sys
|
|
248
|
+
try:
|
|
249
|
+
num, den = tf.num, tf.den
|
|
250
|
+
except AttributeError:
|
|
251
|
+
tf_sys = sig.lti(*sys.to_tf().num, *sys.to_tf().den)
|
|
252
|
+
num, den = tf_sys.num, tf_sys.den
|
|
253
|
+
elif isinstance(sys, tuple) and len(sys) == 2:
|
|
254
|
+
num, den = sys
|
|
255
|
+
else:
|
|
256
|
+
raise TypeError(f"Unsupported type {type(sys)}")
|
|
257
|
+
|
|
258
|
+
return np.atleast_1d(np.array(num, dtype=float)).ravel(), \
|
|
259
|
+
np.atleast_1d(np.array(den, dtype=float)).ravel()
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# ---------------------------------------------------------------------------
|
|
263
|
+
# separtf – proper separation via polynomial equations
|
|
264
|
+
# ---------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
def separtf(sys, ptype='sp'):
|
|
267
|
+
"""
|
|
268
|
+
Proper separation using polynomial equations technique.
|
|
269
|
+
|
|
270
|
+
Equivalent to improper() but uses the polynomial Diophantine approach
|
|
271
|
+
for more robust handling of near-cancellations.
|
|
272
|
+
|
|
273
|
+
Parameters
|
|
274
|
+
----------
|
|
275
|
+
sys : (num, den) tuple
|
|
276
|
+
ptype : str 'sp' or 'p'
|
|
277
|
+
|
|
278
|
+
Returns
|
|
279
|
+
-------
|
|
280
|
+
poly_part : np.ndarray
|
|
281
|
+
proper_part : (num, den) tuple
|
|
282
|
+
"""
|
|
283
|
+
return improper(sys, ptype)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
# ---------------------------------------------------------------------------
|
|
287
|
+
# sumzpk – reliable summation of transfer functions with common poles
|
|
288
|
+
# ---------------------------------------------------------------------------
|
|
289
|
+
|
|
290
|
+
def sumzpk(sys1, sys2):
|
|
291
|
+
"""
|
|
292
|
+
Reliable summation of two transfer functions with common (or
|
|
293
|
+
near-common) poles.
|
|
294
|
+
|
|
295
|
+
Port of MATLAB sumzpk.m: separates any poles/zeros the two operands
|
|
296
|
+
share first, sums only the reduced (non-shared) parts via polynomial
|
|
297
|
+
arithmetic, then reattaches the shared poles exactly -- avoiding the
|
|
298
|
+
numerical cancellation that plain cross-multiplication
|
|
299
|
+
(num1*den2 + num2*den1, den1*den2) suffers when operands share poles
|
|
300
|
+
(duplicated poles come back smeared after re-rooting the raw product).
|
|
301
|
+
Delegates to the already-validated root-list `Zpk.zsum` (used
|
|
302
|
+
throughout the design/polynomial.py pipeline for exactly this reason).
|
|
303
|
+
|
|
304
|
+
Parameters
|
|
305
|
+
----------
|
|
306
|
+
sys1, sys2 : (num, den) tuples, or a scalar
|
|
307
|
+
|
|
308
|
+
Returns
|
|
309
|
+
-------
|
|
310
|
+
(num, den) : result of sys1 + sys2
|
|
311
|
+
"""
|
|
312
|
+
from directsd.zpk.zpk import Zpk
|
|
313
|
+
|
|
314
|
+
def _to_tf(sys):
|
|
315
|
+
if isinstance(sys, (int, float)):
|
|
316
|
+
return np.array([float(sys)]), np.array([1.0])
|
|
317
|
+
num, den = sys
|
|
318
|
+
return (np.atleast_1d(np.array(num, dtype=float)).ravel(),
|
|
319
|
+
np.atleast_1d(np.array(den, dtype=float)).ravel())
|
|
320
|
+
|
|
321
|
+
num1, den1 = _to_tf(sys1)
|
|
322
|
+
num2, den2 = _to_tf(sys2)
|
|
323
|
+
z1 = Zpk.from_tf(num1, den1)
|
|
324
|
+
z2 = Zpk.from_tf(num2, den2)
|
|
325
|
+
return z1.zsum(z2).to_tf()
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
# ---------------------------------------------------------------------------
|
|
329
|
+
# zpk – create ZerosPolesGain model from polynomial objects
|
|
330
|
+
# ---------------------------------------------------------------------------
|
|
331
|
+
|
|
332
|
+
def zpk(N, D=None, T=None):
|
|
333
|
+
"""
|
|
334
|
+
Create a scipy ZerosPolesGain model from polynomial objects.
|
|
335
|
+
|
|
336
|
+
Port of MATLAB ``@poln/zpk``.
|
|
337
|
+
|
|
338
|
+
Parameters
|
|
339
|
+
----------
|
|
340
|
+
N : Poln
|
|
341
|
+
Numerator polynomial.
|
|
342
|
+
D : Poln or scalar, optional
|
|
343
|
+
Denominator polynomial (default: 1).
|
|
344
|
+
T : float, optional
|
|
345
|
+
Sampling period. Required for discrete-time output; inferred from
|
|
346
|
+
``N.var`` when omitted (DT variables z/q/d → T=1 if not given).
|
|
347
|
+
|
|
348
|
+
Returns
|
|
349
|
+
-------
|
|
350
|
+
F : scipy.signal.ZerosPolesGain
|
|
351
|
+
CT or DT ZPK model. The zeros and poles include any roots at the
|
|
352
|
+
origin arising from a shift difference between N and D.
|
|
353
|
+
"""
|
|
354
|
+
import scipy.signal as sig
|
|
355
|
+
from directsd.polynomial.poln import Poln
|
|
356
|
+
from directsd.polynomial.operations import compat, coprime
|
|
357
|
+
|
|
358
|
+
if D is None or (isinstance(D, (int, float)) and D == 1):
|
|
359
|
+
D = Poln(np.array([1.0]), N.var)
|
|
360
|
+
|
|
361
|
+
N, D = compat(N, D)
|
|
362
|
+
# Cancel common factors
|
|
363
|
+
N_r, D_r, _ = coprime(N, D)
|
|
364
|
+
|
|
365
|
+
zeros = list(N_r.roots)
|
|
366
|
+
poles = list(D_r.roots)
|
|
367
|
+
gain = N_r.k / D_r.k
|
|
368
|
+
|
|
369
|
+
# Account for shift difference: shift encodes z^{-shift} factors.
|
|
370
|
+
# Net extra zeros at origin: D.shift - N.shift (positive → extra zeros in N)
|
|
371
|
+
# Net extra poles at origin: N.shift - D.shift (positive → extra poles in N)
|
|
372
|
+
zn = D_r.shift - N_r.shift
|
|
373
|
+
if zn > 0:
|
|
374
|
+
zeros += [0.0] * zn
|
|
375
|
+
elif zn < 0:
|
|
376
|
+
poles += [0.0] * (-zn)
|
|
377
|
+
|
|
378
|
+
z_arr = np.array(zeros, dtype=complex)
|
|
379
|
+
p_arr = np.array(poles, dtype=complex)
|
|
380
|
+
|
|
381
|
+
is_dt = N.is_dt
|
|
382
|
+
if is_dt:
|
|
383
|
+
dt = T if T is not None else 1.0
|
|
384
|
+
return sig.ZerosPolesGain(z_arr, p_arr, gain, dt=dt)
|
|
385
|
+
return sig.ZerosPolesGain(z_arr, p_arr, gain)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
# ---------------------------------------------------------------------------
|
|
389
|
+
# bilinss – bilinear transformation for state-space models
|
|
390
|
+
# ---------------------------------------------------------------------------
|
|
391
|
+
|
|
392
|
+
def bilinss(sys_ss, btype='tustin', T=1.0):
|
|
393
|
+
"""
|
|
394
|
+
Bilinear transformation for a state-space system.
|
|
395
|
+
|
|
396
|
+
Parameters
|
|
397
|
+
----------
|
|
398
|
+
sys_ss : scipy.signal.StateSpace
|
|
399
|
+
btype : str
|
|
400
|
+
'tustin' (default) or 's2z', 'z2s'.
|
|
401
|
+
T : float
|
|
402
|
+
Sampling period for Tustin.
|
|
403
|
+
|
|
404
|
+
Returns
|
|
405
|
+
-------
|
|
406
|
+
sys_out : scipy.signal.StateSpace
|
|
407
|
+
"""
|
|
408
|
+
try:
|
|
409
|
+
import scipy.signal as sig
|
|
410
|
+
import scipy.linalg as la
|
|
411
|
+
except ImportError:
|
|
412
|
+
raise ImportError("scipy is required.")
|
|
413
|
+
|
|
414
|
+
if isinstance(sys_ss, sig.StateSpace):
|
|
415
|
+
A, B, C, D = sys_ss.A, sys_ss.B, sys_ss.C, sys_ss.D
|
|
416
|
+
dt = sys_ss.dt
|
|
417
|
+
else:
|
|
418
|
+
raise TypeError("sys_ss must be a scipy StateSpace")
|
|
419
|
+
|
|
420
|
+
n = A.shape[0]
|
|
421
|
+
|
|
422
|
+
if btype == 'tustin' and (dt is None or dt == 0):
|
|
423
|
+
# Continuous → Discrete: Tustin method
|
|
424
|
+
# z = (1 + s*T/2) / (1 - s*T/2)
|
|
425
|
+
# => A_d = (I + T/2*A) * inv(I - T/2*A)
|
|
426
|
+
alpha = T / 2.0
|
|
427
|
+
IpA = np.eye(n) + alpha * A
|
|
428
|
+
ImA = np.eye(n) - alpha * A
|
|
429
|
+
ImA_inv = la.inv(ImA)
|
|
430
|
+
Ad = ImA_inv @ IpA
|
|
431
|
+
Bd = np.sqrt(T) * ImA_inv @ B
|
|
432
|
+
Cd = np.sqrt(T) * C @ ImA_inv
|
|
433
|
+
Dd = D + alpha * C @ ImA_inv @ B
|
|
434
|
+
return sig.StateSpace(Ad, Bd, Cd, Dd, T)
|
|
435
|
+
|
|
436
|
+
elif btype in ('z2s', 'tustin') and dt is not None and dt != 0:
|
|
437
|
+
# Discrete → Continuous: inverse Tustin
|
|
438
|
+
alpha = T / 2.0
|
|
439
|
+
IpA = np.eye(n) + A
|
|
440
|
+
ImA = np.eye(n) - A
|
|
441
|
+
ImA_inv = la.inv(ImA)
|
|
442
|
+
Ac = (2.0 / T) * ImA_inv @ (A - np.eye(n))
|
|
443
|
+
Bc = np.sqrt(2.0 / T) * ImA_inv @ B
|
|
444
|
+
Cc = np.sqrt(2.0 / T) * C @ ImA_inv
|
|
445
|
+
Dc = D - C @ ImA_inv @ B
|
|
446
|
+
return sig.StateSpace(Ac, Bc, Cc, Dc)
|
|
447
|
+
|
|
448
|
+
else:
|
|
449
|
+
raise ValueError(f"Unsupported bilinear type '{btype}' for given system type")
|