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,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")
@@ -0,0 +1,4 @@
1
+ from directsd.sspace.design import (
2
+ h2reg, hinfreg, sdh2reg, sdhinfreg, sdfast, separss,
3
+ sdh2simple, sdgh2mod, sdnorm, sdsim,
4
+ )