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,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)
@@ -0,0 +1,9 @@
1
+ """
2
+ Transfer-function utilities for DirectSD.
3
+ """
4
+
5
+ from directsd.tf.interconnect import (
6
+ to_lti, nd, mul, neg, add, feedback, nd_mul, nd_neg,
7
+ )
8
+
9
+ __all__ = ['to_lti', 'nd', 'mul', 'neg', 'add', 'feedback', 'nd_mul', 'nd_neg']
@@ -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)
@@ -0,0 +1 @@
1
+ from directsd.zpk.zpk import Zpk, _zpk_snap