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,426 @@
1
+ """
2
+ Diophantine polynomial equation solvers for DirectSD.
3
+
4
+ dioph — X·A + Y·B = C (basic, separate, or nocancel modes)
5
+ dioph2 — X·A + X̃·B + Y·C = 0 (spectral, X̃ = reciprocal of X)
6
+ diophsys — system: X·A1+Y1·B1=C1, X·A2+Y2·B2=C2
7
+ diophsys2 — system: X·A1+X̃·B1+Y1·C1=0, X·A2+X̃·B2+Y2·C2=0
8
+
9
+ Moved out of operations.py so all Diophantine solvers live in one
10
+ importable module. This also fixed a real bug: a second,
11
+ unrelated `diophsys` in polynomial/utils.py (returning a single shared Y for
12
+ both equations, `[X,Y,err]`) had zero callers anywhere in the codebase and no
13
+ test coverage, yet `directsd/__init__.py` exported *that* one as the public
14
+ `directsd.diophsys` — while the correct implementation (matching MATLAB's
15
+ diophsys.m exactly: `[X,Y1,Y2,err,condA]`, separate Y1/Y2, actually used
16
+ internally by _polquad's pipeline) was only reachable via a local aliased
17
+ import in design/polynomial.py. The wrong version has been deleted; this
18
+ diophsys is now the only one and is what `directsd.diophsys` resolves to.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import numpy as np
24
+ import scipy.linalg as la
25
+
26
+ from directsd.polynomial.poln import (
27
+ Poln, coprime as _coprime,
28
+ _strip_lz, _real_if_close,
29
+ )
30
+ from directsd.polynomial.operations import compat, deg, triple
31
+
32
+ _EPS = np.finfo(float).eps
33
+ _SQRT_EPS = np.sqrt(_EPS)
34
+
35
+
36
+ def _toep(coef: np.ndarray, r: int, c: int) -> np.ndarray:
37
+ """
38
+ Build lower-triangular Toeplitz (convolution) matrix.
39
+
40
+ T[i,j] = coef_ascending[i-j] (0-indexed)
41
+ Input: descending polynomial coefficients (highest degree first).
42
+ """
43
+ from directsd.linalg.matrices import toep
44
+ return toep(coef, r, c)
45
+
46
+
47
+ def _lstsq(A: np.ndarray, b: np.ndarray) -> np.ndarray:
48
+ """SVD-based least-squares with iterative refinement (linsys.m 'svd','refine').
49
+
50
+ Truncation tolerance follows MATLAB rank()/linsys.m: max(size(A))·eps
51
+ relative to the largest singular value. A sqrt(eps)-relative cutoff is
52
+ far too aggressive for the block-Sylvester systems built by
53
+ dioph/diophsys: their column blocks are inherently unequally scaled
54
+ (A-block carries the plant gain, often ~1e6; the B-block is a monic
55
+ dLm, ~1), so the smallest genuinely-needed singular value sits ~7
56
+ decades below the largest and gets truncated — the solver then returns
57
+ a garbage minimiser of the wrong subspace even though an exact solution
58
+ exists (root cause of spurious 'pol'→'ss' fallbacks in method
59
+ selection).
60
+ """
61
+ rcond = max(A.shape) * _EPS
62
+ sol, _, _, _ = la.lstsq(A, b, cond=rcond)
63
+ # Iterative refinement (linsys.m lines 131-140: loop while improving)
64
+ res = b - A @ sol
65
+ res_norm = np.linalg.norm(res)
66
+ b_norm = np.linalg.norm(b)
67
+ while res_norm > _SQRT_EPS * b_norm:
68
+ delta, _, _, _ = la.lstsq(A, res, cond=rcond)
69
+ sol_new = sol + delta
70
+ res_new = b - A @ sol_new
71
+ if np.linalg.norm(res_new) >= res_norm:
72
+ break
73
+ sol, res, res_norm = sol_new, res_new, np.linalg.norm(res_new)
74
+ return sol
75
+
76
+
77
+ def _to_coef(p) -> np.ndarray:
78
+ """Return descending coefficient array for Poln or array-like."""
79
+ if isinstance(p, Poln):
80
+ return p.coef.astype(complex)
81
+ return np.atleast_1d(np.asarray(p, dtype=complex)).ravel()
82
+
83
+
84
+ def _from_coef(c: np.ndarray, var: str) -> Poln:
85
+ return Poln(_real_if_close(_strip_lz(np.asarray(c, dtype=complex))), var)
86
+
87
+
88
+ # ---------------------------------------------------------------------------
89
+ # Diophantine solver: X·A + Y·B = C
90
+ # ---------------------------------------------------------------------------
91
+
92
+ def dioph(
93
+ a, b, c,
94
+ dtype: str = 'normal',
95
+ degX: int = -1,
96
+ degY: int = -1,
97
+ ) -> tuple:
98
+ """
99
+ Solve the polynomial Diophantine equation X·A + Y·B = C.
100
+
101
+ Parameters
102
+ ----------
103
+ a, b, c : Poln or array-like
104
+ Polynomial coefficients (descending order) or Poln objects.
105
+ dtype : {'normal', 'nocancel', 'separate'}
106
+ * 'normal' — cancel GCD(A,B,C) first, then coprime factors
107
+ * 'separate' — cancel only coprime factors from (B,C) and (A,C)
108
+ * 'nocancel' — no cancellation
109
+ degX, degY : int
110
+ Desired degrees. -1 means minimal.
111
+
112
+ Returns
113
+ -------
114
+ x, y : Poln
115
+ Solution polynomials.
116
+ err : float
117
+ Residual ||X·A + Y·B - C||.
118
+ condA : float
119
+ Condition number of the Sylvester matrix.
120
+ """
121
+ if dtype not in ('normal', 'nocancel', 'separate'):
122
+ raise ValueError(f"Unknown dtype '{dtype}'")
123
+
124
+ a, b, c = compat(a, b, c)
125
+ var = a.var
126
+ zero = Poln(np.array([0.0]), var)
127
+
128
+ # Degenerate cases
129
+ if a.norm() < _EPS:
130
+ q, r = b.quorem(c) if isinstance(c, Poln) else b.quorem(Poln(c, var))
131
+ return zero, q, r.norm(), 1.0 / _EPS
132
+ if b.norm() < _EPS:
133
+ q, r = a.quorem(c) if isinstance(c, Poln) else a.quorem(Poln(c, var))
134
+ return q, zero, r.norm(), 1.0 / _EPS
135
+
136
+ a0, b0, c0 = a, b, c
137
+
138
+ # Extract cancellable common factors
139
+ ac_fac = bc_fac = Poln(np.array([1.0]), var)
140
+ if dtype == 'separate':
141
+ b, c, bc_fac = _coprime(b, c)
142
+ a, c, ac_fac = _coprime(a, c)
143
+ elif dtype == 'normal':
144
+ result = triple(a, b, c)
145
+ if len(result) == 4:
146
+ a, b, c, _ = result
147
+ else:
148
+ a, b, c = result
149
+ b, c, bc_fac = _coprime(b, c)
150
+ a, c, ac_fac = _coprime(a, c)
151
+
152
+ ca, cb, cc = _to_coef(a), _to_coef(b), _to_coef(c)
153
+
154
+ # Determine polynomial degrees of X and Y
155
+ dA, dB, dC = deg(a), deg(b), deg(c)
156
+ if degX < 0:
157
+ degX = max(dB - 1, 0)
158
+ if degY < 0:
159
+ degY = max(max(dA + degX, dC) - dB, 0)
160
+ if degX < 0:
161
+ degX = 0
162
+ if degY < 0:
163
+ degY = 0
164
+
165
+ m, n = degX + 1, degY + 1
166
+ total = m + n
167
+
168
+ # Sylvester system: [Toep(A)|Toep(B)] * [x_asc; y_asc] = c_asc
169
+ Am = np.hstack([_toep(ca, total, m), _toep(cb, total, n)])
170
+ Cm = _toep(cc, total, 1).ravel()
171
+
172
+ condA = float(np.linalg.cond(Am)) if Am.size > 0 else 1.0
173
+ sol = _lstsq(Am, Cm)
174
+
175
+ # Extract solutions (ascending → descending via [::-1])
176
+ xc = _strip_lz(sol[:m][::-1])
177
+ yc = _strip_lz(sol[m:m+n][::-1])
178
+
179
+ # Multiply back cancelled factors
180
+ x = _from_coef(xc, var) * bc_fac
181
+ y = _from_coef(yc, var) * ac_fac
182
+
183
+ err = float((a0 * x + b0 * y - c0).norm())
184
+ return x, y, err, condA
185
+
186
+
187
+ # ---------------------------------------------------------------------------
188
+ # Diophantine solver: X·A + X̃·B + Y·C = 0
189
+ # ---------------------------------------------------------------------------
190
+
191
+ def dioph2(a, b, c, degX: int = -1) -> tuple:
192
+ """
193
+ Solve X·A + X̃·B + Y·C = 0 (spectral-type Diophantine equation).
194
+
195
+ X̃ is the reciprocal polynomial (reversed coefficients).
196
+ The leading coefficient of X is fixed to 1 (monic constraint).
197
+
198
+ Parameters
199
+ ----------
200
+ a, b, c : Poln or array-like
201
+ degX : int
202
+ Degree of X. Default: deg(C).
203
+
204
+ Returns
205
+ -------
206
+ x, y : Poln
207
+ err : float
208
+ Residual ||X·A + X̃·B + Y·C||.
209
+ """
210
+ a, b, c = compat(a, b, c)
211
+ var = a.var
212
+
213
+ ca, cb, cc = _to_coef(a), _to_coef(b), _to_coef(c)
214
+ dA, dB, dC = deg(a), deg(b), deg(c)
215
+
216
+ if degX < 0:
217
+ degX = dC
218
+ degY = max(dA, dB)
219
+ degAll = max(degX + max(dA, dB), degY + dC)
220
+
221
+ total = degAll + 1
222
+ mX = degX + 1
223
+ nY = degY + 1
224
+
225
+ # Coefficient matrix for X: toep(A) + fliplr(toep(B))
226
+ # fliplr(toep(B)) acts on the reversed-X coefficient vector
227
+ # to produce the X̃·B contribution.
228
+ AmX = _toep(ca, total, mX) + np.fliplr(_toep(cb, total, mX))
229
+ AmY = _toep(cc, total, nY)
230
+
231
+ # System: [AmY | AmX] * [y_asc; x_asc_without_leading] = -AmX[:, -1]
232
+ # Fix highest coefficient of X to 1 (monic)
233
+ Am = np.hstack([AmY, AmX])
234
+ Cm = -Am[:, -1]
235
+ Am = Am[:, :-1]
236
+
237
+ sol = _lstsq(Am, Cm)
238
+ sol = _strip_lz(sol.ravel())
239
+
240
+ # Extract Y and X (ascending → descending)
241
+ y_asc = sol[:nY]
242
+ x_asc = np.concatenate([sol[nY:], [1.0]]) # append the fixed leading coef
243
+
244
+ xc = _strip_lz(x_asc[::-1])
245
+ yc = _strip_lz(y_asc[::-1])
246
+
247
+ x = _from_coef(xc, var)
248
+ y = _from_coef(yc, var)
249
+
250
+ err = float((a * x + b * x.reciprocal() + c * y).norm())
251
+ return x, y, err
252
+
253
+
254
+ # ---------------------------------------------------------------------------
255
+ # System of two Diophantine equations: X·Ai + Yi·Bi = Ci, i=1,2
256
+ # ---------------------------------------------------------------------------
257
+
258
+ def diophsys(a1, b1, c1, a2, b2, c2) -> tuple:
259
+ """
260
+ Solve the system:
261
+ X·A1 + Y1·B1 = C1
262
+ X·A2 + Y2·B2 = C2
263
+
264
+ X is shared; Y1, Y2 are separate. X is of minimal degree.
265
+
266
+ Solvability condition: (A2·C1 - A1·C2) must be divisible by GCD(B1, B2).
267
+
268
+ Returns
269
+ -------
270
+ x, y1, y2 : Poln
271
+ err : float
272
+ ||X·A1+Y1·B1-C1|| + ||X·A2+Y2·B2-C2||
273
+ condA : float
274
+ """
275
+ a1, b1, c1, a2, b2, c2 = compat(a1, b1, c1, a2, b2, c2)
276
+ var = a1.var
277
+
278
+ # Extract GCD of B1 and B2 to determine degree of X
279
+ b1r, b2r, v = _coprime(b1, b2) # b1 = b1r * v, b2 = b2r * v (sort of)
280
+ # In MATLAB: [B1r,B2r,V] = coprime(PB1, PB2) → B1 = B1r*V, B2 = B2r*V
281
+
282
+ # Check solvability (warn if not met, but proceed)
283
+ p_check = a2 * c1 - a1 * c2
284
+ if p_check.norm() > _SQRT_EPS:
285
+ _, rem = p_check.quorem(v)
286
+ if rem.norm() > 1e-6 * p_check.norm():
287
+ import warnings
288
+ warnings.warn(
289
+ f"A2·C1-A1·C2 may not be divisible by GCD(B1,B2) (rel.err={rem.norm()/p_check.norm():.2e})"
290
+ )
291
+
292
+ ca1, cb1, cc1 = _to_coef(a1), _to_coef(b1), _to_coef(c1)
293
+ ca2, cb2, cc2 = _to_coef(a2), _to_coef(b2), _to_coef(c2)
294
+
295
+ dA1, dB1, dC1 = deg(a1), deg(b1), deg(c1)
296
+ dA2, dB2, dC2 = deg(a2), deg(b2), deg(c2)
297
+
298
+ # Degree of X is deg(B1*B2r) - 1 = deg(lcm(B1,B2)) - 1
299
+ # b = B1 * B2r has degree dB1 + deg(B2r)
300
+ b_lcm = b1 * b2r # lcm of B1 and B2
301
+ degX = max(deg(b_lcm) - 1, 0)
302
+
303
+ degY1 = max(dA1 + deg(b2r) - 1, 0)
304
+ degY2 = max(dA2 + deg(b1r) - 1, 0)
305
+
306
+ # Adjust if C polynomials exceed the default degree estimate
307
+ if degX + dA1 < dC1:
308
+ degY1 = max(dC1 - dB1, degY1)
309
+ if degX + dA2 < dC2:
310
+ degY2 = max(dC2 - dB2, degY2)
311
+
312
+ m = degX + 1
313
+ n1 = degY1 + 1
314
+ n2 = degY2 + 1
315
+ deg1 = max(degX + max(dA1, dB1), dC1)
316
+ deg2 = max(degX + max(dA2, dB2), dC2)
317
+ r1, r2 = deg1 + 1, deg2 + 1
318
+
319
+ # Block Sylvester matrix:
320
+ # [Toep(A1,r1,m) | Toep(B1,r1,n1) | 0 ] [x ] [C1]
321
+ # [Toep(A2,r2,m) | 0 | Toep(B2,r2,n2) ] [y1 ] = [C2]
322
+ # [y2 ]
323
+ Am1X = _toep(ca1, r1, m)
324
+ Am1Y1 = _toep(cb1, r1, n1)
325
+ Am2X = _toep(ca2, r2, m)
326
+ Am2Y2 = _toep(cb2, r2, n2)
327
+
328
+ Am = np.block([
329
+ [Am1X, Am1Y1, np.zeros((r1, n2))],
330
+ [Am2X, np.zeros((r2, n1)), Am2Y2],
331
+ ])
332
+ Cm = np.concatenate([
333
+ _toep(cc1, r1, 1).ravel(),
334
+ _toep(cc2, r2, 1).ravel(),
335
+ ])
336
+
337
+ condA = float(np.linalg.cond(Am))
338
+ sol = _lstsq(Am, Cm)
339
+
340
+ xc = _strip_lz(sol[:m][::-1])
341
+ y1c = _strip_lz(sol[m:m+n1][::-1])
342
+ y2c = _strip_lz(sol[m+n1:m+n1+n2][::-1])
343
+
344
+ x = _from_coef(xc, var)
345
+ y1 = _from_coef(y1c, var)
346
+ y2 = _from_coef(y2c, var)
347
+
348
+ err = float((a1*x + b1*y1 - c1).norm() + (a2*x + b2*y2 - c2).norm())
349
+ return x, y1, y2, err, condA
350
+
351
+
352
+ # ---------------------------------------------------------------------------
353
+ # System of two spectral-type equations: X·Ai + X̃·Bi + Yi·Ci = 0, i=1,2
354
+ # ---------------------------------------------------------------------------
355
+
356
+ def diophsys2(a1, b1, c1, a2, b2, c2, degX: int = -1) -> tuple:
357
+ """
358
+ Solve the system:
359
+ X·A1 + X̃·B1 + Y1·C1 = 0
360
+ X·A2 + X̃·B2 + Y2·C2 = 0
361
+
362
+ X̃ is the reciprocal of X. The leading coefficient of X is fixed to 1.
363
+
364
+ Returns
365
+ -------
366
+ x, y1, y2 : Poln
367
+ err : float
368
+ ||X·A1+X̃·B1+Y1·C1|| + ||X·A2+X̃·B2+Y2·C2||
369
+ """
370
+ a1, b1, c1, a2, b2, c2 = compat(a1, b1, c1, a2, b2, c2)
371
+ var = a1.var
372
+
373
+ ca1, cb1, cc1 = _to_coef(a1), _to_coef(b1), _to_coef(c1)
374
+ ca2, cb2, cc2 = _to_coef(a2), _to_coef(b2), _to_coef(c2)
375
+
376
+ dA1, dB1, dC1 = deg(a1), deg(b1), deg(c1)
377
+ dA2, dB2, dC2 = deg(a2), deg(b2), deg(c2)
378
+
379
+ if degX < 0:
380
+ degX = max(dC1, dC2)
381
+
382
+ deg1 = max(degX + max(dA1, dB1), dC1)
383
+ deg2 = max(degX + max(dA2, dB2), dC2)
384
+ degY1 = deg1 - dC1
385
+ degY2 = deg2 - dC2
386
+
387
+ r1, r2 = deg1 + 1, deg2 + 1
388
+ mX = degX + 1
389
+ nY1 = degY1 + 1
390
+ nY2 = degY2 + 1
391
+
392
+ # X coefficient matrix: toep(Ai) + fliplr(toep(Bi)) for i=1,2
393
+ AmX1 = _toep(ca1, r1, mX) + np.fliplr(_toep(cb1, r1, mX))
394
+ AmY1 = _toep(cc1, r1, nY1)
395
+ AmX2 = _toep(ca2, r2, mX) + np.fliplr(_toep(cb2, r2, mX))
396
+ AmY2 = _toep(cc2, r2, nY2)
397
+
398
+ # Flip columns as in MATLAB: fliplr(AmX) puts highest-coef column first
399
+ # Fix highest coef of X to 1 (remove first column = highest coef)
400
+ Am = np.block([
401
+ [np.fliplr(AmX1), np.fliplr(AmY1), np.zeros((r1, nY2))],
402
+ [np.fliplr(AmX2), np.zeros((r2, nY1)), np.fliplr(AmY2)],
403
+ ])
404
+ Cm = -Am[:, 0]
405
+ Am = Am[:, 1:]
406
+
407
+ sol = _lstsq(Am, Cm)
408
+
409
+ # Extract: first column of X is fixed to 1 (highest coef in descending)
410
+ x_desc = np.concatenate([[1.0], sol[:degX]]) # descending, highest first
411
+ y1_asc = sol[degX:degX+nY1]
412
+ y2_asc = sol[degX+nY1:degX+nY1+nY2]
413
+
414
+ xc = _strip_lz(x_desc)
415
+ y1c = _strip_lz(y1_asc[::-1])
416
+ y2c = _strip_lz(y2_asc[::-1])
417
+
418
+ x = _from_coef(xc, var)
419
+ y1 = _from_coef(y1c, var)
420
+ y2 = _from_coef(y2c, var)
421
+
422
+ err = float(
423
+ (a1*x + b1*x.reciprocal() + c1*y1).norm()
424
+ + (a2*x + b2*x.reciprocal() + c2*y2).norm()
425
+ )
426
+ return x, y1, y2, err
@@ -0,0 +1,247 @@
1
+ """
2
+ Polynomial operations for DirectSD — general polynomial algebra helpers
3
+ (degree, GCD/coprime factoring, triple cancellation, factorization, etc.).
4
+
5
+ Diophantine equation solvers (dioph, dioph2, diophsys, diophsys2) live in
6
+ directsd.polynomial.diophantine, so all Diophantine solvers are importable
7
+ from one place.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import numpy as np
13
+
14
+ from directsd.polynomial.poln import (
15
+ Poln, coprime as _coprime,
16
+ _findzero, _real_if_close,
17
+ )
18
+
19
+ _EPS = np.finfo(float).eps
20
+ _SQRT_EPS = np.sqrt(_EPS)
21
+
22
+ # NumPy version shim (kept for backward compat with existing tests)
23
+ _trapezoid = getattr(np, 'trapezoid', getattr(np, 'trapz', None))
24
+
25
+
26
+ # ---------------------------------------------------------------------------
27
+ # Utility helpers (keep public aliases for other modules)
28
+ # ---------------------------------------------------------------------------
29
+
30
+ def compat(*polys):
31
+ """Coerce all arguments to share the same variable, widening raw scalars/
32
+ arrays/scipy LTI objects/(num,den) tuples to `Poln` along the way.
33
+
34
+ NOT a re-export of `poln.compat` -- deliberately a
35
+ separate, WIDER function: `poln.compat` is an internal-use-only helper
36
+ for `Poln`'s own dunder methods (always called with at least one `Poln`
37
+ argument already in hand, e.g. `self`), so it can afford to pass
38
+ non-Poln inputs through unchanged when no `Poln` is present at all. This
39
+ version is the public-facing entry point for `coprime`/`gcd`/`triple`
40
+ and `directsd.polynomial.diophantine`'s solvers, which routinely receive
41
+ raw arrays, scipy `lti`/`dlti` objects, or `(num,den)` tuples and need
42
+ them ALWAYS promoted to `Poln` (defaulting to `var='s'` if no `Poln`
43
+ argument is present at all) before any polynomial algebra can proceed.
44
+ """
45
+ import scipy.signal as sig
46
+ var = None
47
+ for p in polys:
48
+ if isinstance(p, Poln):
49
+ var = p.var
50
+ break
51
+ if var is None:
52
+ var = 's'
53
+ out = []
54
+ for p in polys:
55
+ if isinstance(p, Poln):
56
+ out.append(p if p.var == var else Poln(p.coef, var, p.shift))
57
+ elif isinstance(p, (int, float, complex, np.number)):
58
+ out.append(Poln(np.array([complex(p)]), var))
59
+ elif isinstance(p, np.ndarray) and p.ndim == 0:
60
+ out.append(Poln(np.array([float(p)]), var))
61
+ elif isinstance(p, np.ndarray):
62
+ out.append(Poln(p.ravel(), var))
63
+ elif isinstance(p, (sig.lti, sig.dlti)):
64
+ out.append(Poln(np.atleast_1d(p.den).ravel(), var))
65
+ elif isinstance(p, tuple) and len(p) == 2:
66
+ out.append(Poln(np.atleast_1d(p[1]).ravel(), var))
67
+ else:
68
+ raise TypeError(f"Cannot coerce {type(p)} to Poln")
69
+ return out[0] if len(out) == 1 else tuple(out)
70
+
71
+
72
+ def deg(p) -> int:
73
+ if isinstance(p, Poln):
74
+ return p.degree
75
+ return len(np.atleast_1d(np.asarray(p))) - 1
76
+
77
+
78
+ def striplz(coef, tol=_SQRT_EPS):
79
+ arr = np.atleast_1d(np.array(coef, dtype=complex))
80
+ while arr.size > 1 and abs(arr[0]) <= tol:
81
+ arr = arr[1:]
82
+ return np.real_if_close(arr, tol=1e6)
83
+
84
+
85
+ def coprime(A, B, tol=None):
86
+ """Cancel common roots of A and B; returns (A/gcd, B/gcd, gcd)."""
87
+ if tol is None:
88
+ tol = _SQRT_EPS
89
+ A, B = compat(A, B)
90
+ return _coprime(A, B, tol)
91
+
92
+
93
+ def gcd(A, B, *rest):
94
+ """Monic GCD of two (or more) polynomials."""
95
+ _, _, G = _coprime(*compat(A, B))
96
+ for C in rest:
97
+ _, _, G = _coprime(*compat(G, C))
98
+ return G.monic()
99
+
100
+
101
+ def triple(A, B, C, tol=None):
102
+ """
103
+ Extract common GCD of three polynomials simultaneously.
104
+
105
+ Returns (A/g, B/g, C/g, g) where g is the monic common factor.
106
+ Equivalent to MATLAB triple().
107
+ """
108
+ if tol is None:
109
+ tol = _SQRT_EPS
110
+ A, B, C = compat(A, B, C)
111
+ var = A.var
112
+
113
+ g = Poln(np.array([1.0]), var)
114
+ if A.degree < 1 or A.norm() < 1e-10:
115
+ if A.norm() < 1e-10:
116
+ B, C, g = _coprime(B, C, tol)
117
+ return A, B, C, g
118
+ if B.degree < 1 or B.norm() < 1e-10:
119
+ if B.norm() < 1e-10:
120
+ A, C, g = _coprime(A, C, tol)
121
+ return A, B, C, g
122
+ if C.degree < 1 or C.norm() < 1e-10:
123
+ if C.norm() < 1e-10:
124
+ A, B, g = _coprime(A, B, tol)
125
+ return A, B, C, g
126
+
127
+ rA = list(A.roots)
128
+ rB = list(B.roots)
129
+ rC = list(C.roots)
130
+ kA, kB, kC = A.k, B.k, C.k
131
+ rG: list = []
132
+
133
+ i = 0
134
+ while i < len(rA):
135
+ if not rB or not rC:
136
+ break
137
+ R = rA[i]
138
+ tolR = max(tol, tol * abs(R))
139
+ dB = [abs(R - b) for b in rB]
140
+ dC = [abs(R - c) for c in rC]
141
+ jB, jC = int(np.argmin(dB)), int(np.argmin(dC))
142
+ if dB[jB] < tolR and dC[jC] < tolR:
143
+ rG.append(R)
144
+ rA.pop(i)
145
+ rB.pop(jB)
146
+ rC.pop(jC)
147
+ else:
148
+ i += 1
149
+
150
+ if not rG:
151
+ return A, B, C, g
152
+
153
+ cA = _real_if_close(kA * np.poly(rA)) if rA else np.array([kA])
154
+ cB = _real_if_close(kB * np.poly(rB)) if rB else np.array([kB])
155
+ cC = _real_if_close(kC * np.poly(rC)) if rC else np.array([kC])
156
+ cG = _real_if_close(np.poly(rG))
157
+ return (
158
+ Poln(cA, var, A.shift),
159
+ Poln(cB, var, B.shift),
160
+ Poln(cC, var, C.shift),
161
+ Poln(cG, var),
162
+ )
163
+
164
+
165
+ def factor(f, ftype=None):
166
+ """Split f into (stable, unstable, neutral) polynomial factors."""
167
+ if not isinstance(f, Poln):
168
+ f = Poln(np.atleast_1d(f))
169
+ if ftype is None:
170
+ ftype = 'd' if f.is_dt else 's'
171
+ if ftype not in ('s', 'z', 'd'):
172
+ raise ValueError(f"Unknown type '{ftype}'")
173
+ var = f.var
174
+ rts = f.roots
175
+ k = f.k
176
+ tol = _SQRT_EPS
177
+ if ftype == 's':
178
+ ms = np.real(rts) < -tol
179
+ mu = np.real(rts) > tol
180
+ elif ftype == 'z':
181
+ ms = np.abs(rts) < 1 - tol
182
+ mu = np.abs(rts) > 1 + tol
183
+ else:
184
+ ms = np.abs(rts) > 1 + tol
185
+ mu = np.abs(rts) < 1 - tol
186
+ m0 = ~ms & ~mu
187
+
188
+ def _b(r, gain):
189
+ c = gain * np.poly(r) if len(r) > 0 else np.array([gain])
190
+ return Poln(np.real_if_close(c, tol=1e6), var)
191
+
192
+ return _b(rts[ms], k), _b(rts[mu], 1.0), _b(rts[m0], 1.0)
193
+
194
+
195
+ def recip(p):
196
+ if isinstance(p, Poln):
197
+ return p.reciprocal()
198
+ return _findzero(np.atleast_1d(np.array(p))[::-1])
199
+
200
+
201
+ def vec(p):
202
+ if isinstance(p, Poln):
203
+ return p.coef.copy()
204
+ return np.atleast_1d(np.array(p)).ravel()
205
+
206
+
207
+ def delzero(A, tol=None):
208
+ """
209
+ Remove zeros at the origin from a polynomial.
210
+
211
+ Port of MATLAB ``@poln/delzero`` and ``private/delzero``.
212
+
213
+ Parameters
214
+ ----------
215
+ A : Poln or array-like
216
+ Input polynomial or coefficient array.
217
+ tol : float, optional
218
+ Tolerance for identifying zero roots. Default: machine epsilon.
219
+
220
+ Returns
221
+ -------
222
+ B : Poln or np.ndarray
223
+ Reduced polynomial / array with zero roots removed.
224
+ nz : int
225
+ Number of zeros removed.
226
+ """
227
+ if tol is None:
228
+ tol = _EPS
229
+
230
+ if isinstance(A, Poln):
231
+ z = A.roots
232
+ mask = np.abs(z) < tol
233
+ nz = int(np.sum(mask))
234
+ if nz == 0:
235
+ return A, 0
236
+ z_new = z[~mask]
237
+ k = A.k
238
+ c = _real_if_close(k * np.poly(z_new)) if z_new.size > 0 else np.array([k])
239
+ return Poln(c, A.var, A.shift), nz
240
+
241
+ # Plain array: strip trailing near-zero coefficients
242
+ arr = np.atleast_1d(np.array(A, dtype=float)).ravel()
243
+ nz = 0
244
+ while arr.size > 1 and abs(arr[-1]) <= tol:
245
+ arr = arr[:-1]
246
+ nz += 1
247
+ return arr, nz