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
directsd/sspace/plant.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GeneralizedPlant — the standard LFT augmented plant for sampled-data H2/Hinf design.
|
|
3
|
+
|
|
4
|
+
In the DirectSD toolbox (and H∞ control theory generally), the *generalized plant*
|
|
5
|
+
(also called augmented plant) partitions the plant into four sub-blocks::
|
|
6
|
+
|
|
7
|
+
[z] [P11 P12] [w]
|
|
8
|
+
[y] = [P21 P22] [u]
|
|
9
|
+
|
|
10
|
+
where:
|
|
11
|
+
w — exogenous inputs (disturbances, reference signals)
|
|
12
|
+
u — control inputs
|
|
13
|
+
z — performance outputs (signals to be minimised)
|
|
14
|
+
y — measurement outputs (signals available to the controller)
|
|
15
|
+
|
|
16
|
+
This class provides the Python equivalent of MATLAB's natural matrix concatenation::
|
|
17
|
+
|
|
18
|
+
sys = [F*Fw F ← performance rows
|
|
19
|
+
0 rho ← (can be multiple performance rows)
|
|
20
|
+
-F*Fw -F] ← measurement row
|
|
21
|
+
|
|
22
|
+
Usage::
|
|
23
|
+
|
|
24
|
+
from directsd import GeneralizedPlant
|
|
25
|
+
|
|
26
|
+
sys = GeneralizedPlant([
|
|
27
|
+
[F*Fw, F ],
|
|
28
|
+
[0, rho],
|
|
29
|
+
[-F*Fw, -F ],
|
|
30
|
+
]) # 3-output × 2-input, n_meas=1, n_ctrl=1 by default
|
|
31
|
+
|
|
32
|
+
K, cost = sdh2(sys, T=0.1)
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
import numpy as np
|
|
36
|
+
import scipy.signal as sig
|
|
37
|
+
from directsd.tf.interconnect import to_lti
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class GeneralizedPlant:
|
|
41
|
+
"""
|
|
42
|
+
Standard generalized (augmented) plant for sampled-data H2/Hinf design.
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
rows : list[list] or scipy.signal.StateSpace
|
|
47
|
+
Either a 2-D grid of SISO blocks (scalars, ``(num, den)`` tuples, or
|
|
48
|
+
``scipy.signal.lti`` objects), or a pre-built ``StateSpace``.
|
|
49
|
+
The grid shape determines the total number of outputs and inputs.
|
|
50
|
+
n_perf : int, optional
|
|
51
|
+
Number of performance outputs (z-rows). Default: ``nout - n_meas``.
|
|
52
|
+
n_meas : int, optional
|
|
53
|
+
Number of measurement outputs (y-rows). Default: ``1``.
|
|
54
|
+
n_dist : int, optional
|
|
55
|
+
Number of exogenous inputs (w-columns). Default: ``nin - n_ctrl``.
|
|
56
|
+
n_ctrl : int, optional
|
|
57
|
+
Number of control inputs (u-columns). Default: ``1``.
|
|
58
|
+
|
|
59
|
+
Attributes
|
|
60
|
+
----------
|
|
61
|
+
n_perf, n_meas, n_dist, n_ctrl : int
|
|
62
|
+
Partition sizes.
|
|
63
|
+
A, B, C, D : numpy.ndarray
|
|
64
|
+
State-space matrices (delegated from the internal StateSpace).
|
|
65
|
+
|
|
66
|
+
Examples
|
|
67
|
+
--------
|
|
68
|
+
Standard 1-DOF H2 plant (3 outputs, 2 inputs)::
|
|
69
|
+
|
|
70
|
+
sys = GeneralizedPlant([
|
|
71
|
+
[_mul(Fw, F), F ], # performance: weighted output
|
|
72
|
+
[0, rho], # performance: control cost
|
|
73
|
+
[_neg(_mul(Fw, F)), _neg(F)], # measurement
|
|
74
|
+
])
|
|
75
|
+
|
|
76
|
+
2-DOF plant (3 outputs, 2 inputs, n_meas=2)::
|
|
77
|
+
|
|
78
|
+
sys = GeneralizedPlant([
|
|
79
|
+
[_neg(F), _neg(F)], # performance
|
|
80
|
+
[1, 0 ], # measurement 1: reference
|
|
81
|
+
[0, _neg(F)], # measurement 2: plant output
|
|
82
|
+
], n_meas=2)
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __init__(self, rows, *, n_perf=None, n_meas=1, n_dist=None, n_ctrl=1):
|
|
86
|
+
if isinstance(rows, sig.StateSpace):
|
|
87
|
+
ss = rows
|
|
88
|
+
else:
|
|
89
|
+
ss = _build_ss(rows)
|
|
90
|
+
|
|
91
|
+
nout = ss.C.shape[0]
|
|
92
|
+
nin = ss.B.shape[1]
|
|
93
|
+
|
|
94
|
+
self._ss = ss
|
|
95
|
+
self.n_meas = int(n_meas)
|
|
96
|
+
self.n_ctrl = int(n_ctrl)
|
|
97
|
+
self.n_perf = int(nout - n_meas) if n_perf is None else int(n_perf)
|
|
98
|
+
self.n_dist = int(nin - n_ctrl) if n_dist is None else int(n_dist)
|
|
99
|
+
|
|
100
|
+
# ── State-space matrix access ──────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def A(self):
|
|
104
|
+
return self._ss.A
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def B(self):
|
|
108
|
+
return self._ss.B
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def C(self):
|
|
112
|
+
return self._ss.C
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def D(self):
|
|
116
|
+
return self._ss.D
|
|
117
|
+
|
|
118
|
+
# ── Interop ────────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
def to_statespace(self) -> sig.StateSpace:
|
|
121
|
+
"""Return the underlying ``scipy.signal.StateSpace``."""
|
|
122
|
+
return self._ss
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def P22(self) -> sig.StateSpace:
|
|
126
|
+
"""
|
|
127
|
+
Extract P22 sub-block (lower-right corner: y ← u channel).
|
|
128
|
+
|
|
129
|
+
Returns a ``StateSpace`` with shared A matrix and the last
|
|
130
|
+
``n_meas`` output rows and last ``n_ctrl`` input columns.
|
|
131
|
+
"""
|
|
132
|
+
A = self._ss.A
|
|
133
|
+
B2 = self._ss.B[:, -self.n_ctrl:]
|
|
134
|
+
C2 = self._ss.C[-self.n_meas:, :]
|
|
135
|
+
D22 = self._ss.D[-self.n_meas:, -self.n_ctrl:]
|
|
136
|
+
return sig.StateSpace(A, B2, C2, D22)
|
|
137
|
+
|
|
138
|
+
def __repr__(self) -> str:
|
|
139
|
+
nout = self._ss.C.shape[0]
|
|
140
|
+
nin = self._ss.B.shape[1]
|
|
141
|
+
n = self._ss.A.shape[0]
|
|
142
|
+
return (
|
|
143
|
+
f"GeneralizedPlant({nout}×{nin}, states={n}, "
|
|
144
|
+
f"n_perf={self.n_perf}, n_meas={self.n_meas}, "
|
|
145
|
+
f"n_dist={self.n_dist}, n_ctrl={self.n_ctrl})"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ── Internal block-matrix assembler ───────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
_coerce_lti = to_lti # alias used by _block_abcd below
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _block_abcd(P):
|
|
155
|
+
"""Convert a SISO block to (A, B, C, D) arrays with shape (n,), (n,1), (1,n), (1,1)."""
|
|
156
|
+
if isinstance(P, list) and len(P) == 1:
|
|
157
|
+
P = P[0]
|
|
158
|
+
if np.isscalar(P) or (isinstance(P, np.ndarray) and P.ndim == 0):
|
|
159
|
+
return (np.zeros((0, 0)), np.zeros((0, 1)),
|
|
160
|
+
np.zeros((1, 0)), np.array([[float(P)]]))
|
|
161
|
+
ss = _coerce_lti(P).to_ss()
|
|
162
|
+
n = ss.A.shape[0]
|
|
163
|
+
return (ss.A,
|
|
164
|
+
ss.B.reshape(n, 1),
|
|
165
|
+
ss.C.reshape(1, n),
|
|
166
|
+
ss.D.reshape(1, 1))
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _build_ss(rows) -> sig.StateSpace:
|
|
170
|
+
"""
|
|
171
|
+
Assemble a MIMO StateSpace from a 2-D list of SISO blocks.
|
|
172
|
+
|
|
173
|
+
Each block gets independent state variables (block-diagonal A matrix).
|
|
174
|
+
"""
|
|
175
|
+
nr = len(rows)
|
|
176
|
+
nc = len(rows[0])
|
|
177
|
+
blk = [[_block_abcd(rows[i][j]) for j in range(nc)] for i in range(nr)]
|
|
178
|
+
ns = [[blk[i][j][0].shape[0] for j in range(nc)] for i in range(nr)]
|
|
179
|
+
ntot = sum(ns[i][j] for i in range(nr) for j in range(nc))
|
|
180
|
+
|
|
181
|
+
A = np.zeros((ntot, ntot))
|
|
182
|
+
B = np.zeros((ntot, nc))
|
|
183
|
+
C = np.zeros((nr, ntot))
|
|
184
|
+
D = np.zeros((nr, nc))
|
|
185
|
+
|
|
186
|
+
off = 0
|
|
187
|
+
for j in range(nc):
|
|
188
|
+
for i in range(nr):
|
|
189
|
+
Aij, Bij, Cij, Dij = blk[i][j]
|
|
190
|
+
nij = ns[i][j]
|
|
191
|
+
if nij > 0:
|
|
192
|
+
A[off:off + nij, off:off + nij] = Aij
|
|
193
|
+
B[off:off + nij, j:j + 1] = Bij
|
|
194
|
+
C[i:i + 1, off:off + nij] = Cij
|
|
195
|
+
D[i, j] = Dij[0, 0]
|
|
196
|
+
off += nij
|
|
197
|
+
|
|
198
|
+
return sig.StateSpace(A, B, C, D)
|
directsd/tf/__init__.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Transfer-function block diagram arithmetic.
|
|
3
|
+
|
|
4
|
+
Provides scalar/tuple/(num,den)/lti-compatible operations for building
|
|
5
|
+
interconnected SISO transfer functions before assembling them into a
|
|
6
|
+
GeneralizedPlant.
|
|
7
|
+
|
|
8
|
+
Functions
|
|
9
|
+
---------
|
|
10
|
+
to_lti -- coerce any TF-like input to scipy.signal.lti
|
|
11
|
+
nd -- extract (num, den) 1-D arrays from any TF-like input
|
|
12
|
+
mul -- series (cascade) connection: P1 * P2 * ...
|
|
13
|
+
neg -- negate: -P
|
|
14
|
+
add -- parallel (sum) connection: P + Q
|
|
15
|
+
feedback -- unity negative feedback: P / (1 + P)
|
|
16
|
+
nd_mul -- series connection, returns (num, den) tuple
|
|
17
|
+
nd_neg -- negate, returns (num, den) tuple
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
import scipy.signal as sig
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def to_lti(P):
|
|
25
|
+
"""
|
|
26
|
+
Coerce any TF-like input to a ``scipy.signal.lti`` object.
|
|
27
|
+
|
|
28
|
+
Accepts: scalar, ``(num, den)`` tuple, ``scipy.signal.lti`` subclass,
|
|
29
|
+
``TransferFunction``, ``ZerosPolesGain``, or ``StateSpace``.
|
|
30
|
+
"""
|
|
31
|
+
if isinstance(P, (sig.lti, sig.TransferFunction,
|
|
32
|
+
sig.ZerosPolesGain, sig.StateSpace)):
|
|
33
|
+
return P
|
|
34
|
+
if isinstance(P, tuple) and len(P) == 2:
|
|
35
|
+
return sig.TransferFunction(*P)
|
|
36
|
+
if np.isscalar(P):
|
|
37
|
+
return sig.TransferFunction([float(P)], [1.0])
|
|
38
|
+
raise TypeError(f"Cannot coerce {type(P)} to lti")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def nd(P):
|
|
42
|
+
"""
|
|
43
|
+
Extract ``(num, den)`` 1-D arrays from any TF-like input.
|
|
44
|
+
|
|
45
|
+
Accepts the same types as :func:`to_lti`, plus raw ``(num, den)`` tuples.
|
|
46
|
+
"""
|
|
47
|
+
if np.isscalar(P):
|
|
48
|
+
return np.array([float(P)]), np.array([1.0])
|
|
49
|
+
if isinstance(P, tuple) and len(P) == 2:
|
|
50
|
+
return (np.atleast_1d(np.array(P[0], float)).ravel(),
|
|
51
|
+
np.atleast_1d(np.array(P[1], float)).ravel())
|
|
52
|
+
if hasattr(P, 'to_tf'):
|
|
53
|
+
P = P.to_tf()
|
|
54
|
+
if isinstance(P, (sig.lti, sig.TransferFunction)):
|
|
55
|
+
return (np.atleast_1d(np.array(P.num, float)).ravel(),
|
|
56
|
+
np.atleast_1d(np.array(P.den, float)).ravel())
|
|
57
|
+
raise TypeError(f"Cannot extract (num, den) from {type(P)}")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def mul(*plants):
|
|
61
|
+
"""Series (cascade) connection of two or more transfer functions."""
|
|
62
|
+
n, d = nd(plants[0])
|
|
63
|
+
for p in plants[1:]:
|
|
64
|
+
pn, pd = nd(p)
|
|
65
|
+
n = np.polymul(n, pn)
|
|
66
|
+
d = np.polymul(d, pd)
|
|
67
|
+
return sig.TransferFunction(n, d)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def neg(P):
|
|
71
|
+
"""Negate a transfer function: returns ``-P``."""
|
|
72
|
+
n, d = nd(P)
|
|
73
|
+
return sig.TransferFunction(-n, d)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def add(P, Q):
|
|
77
|
+
"""Parallel (sum) connection: returns ``P + Q``."""
|
|
78
|
+
pn, pd = nd(P)
|
|
79
|
+
qn, qd = nd(Q)
|
|
80
|
+
return sig.TransferFunction(
|
|
81
|
+
np.polyadd(np.polymul(pn, qd), np.polymul(qn, pd)),
|
|
82
|
+
np.polymul(pd, qd),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def feedback(P):
|
|
87
|
+
"""Unity negative feedback: returns ``P / (1 + P)``."""
|
|
88
|
+
pn, pd = nd(P)
|
|
89
|
+
return sig.TransferFunction(pn, np.polyadd(pd, pn))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def nd_mul(*plants):
|
|
93
|
+
"""Series connection returning a ``(num, den)`` tuple."""
|
|
94
|
+
n, d = nd(plants[0])
|
|
95
|
+
for p in plants[1:]:
|
|
96
|
+
pn, pd = nd(p)
|
|
97
|
+
n = np.polymul(n, pn)
|
|
98
|
+
d = np.polymul(d, pd)
|
|
99
|
+
return (n, d)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def nd_neg(P):
|
|
103
|
+
"""Negate, returning a ``(num, den)`` tuple."""
|
|
104
|
+
n, d = nd(P)
|
|
105
|
+
return (-n, d)
|
directsd/zpk/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from directsd.zpk.zpk import Zpk, _zpk_snap
|