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,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
|