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,1658 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Advanced global optimization routines for DirectSD.
|
|
3
|
+
|
|
4
|
+
Ports of dsdglopt (K. Polyakov, 2006):
|
|
5
|
+
Utilities : updateopt, uniproj, u2range, randbeta, randgamma, sa_testfun
|
|
6
|
+
Encoding : val2bin, bin2val, coord2hilb, hilb2coord
|
|
7
|
+
Sector maps : r2range, r1range, admproj, par2cp, cp2par, guesspoles
|
|
8
|
+
Controllers : k2ksi, go_par2k, f_sdh2p, f_sdl2p, go_sdh2p, go_sdl2p
|
|
9
|
+
Optimizers : sasimplex, arandsearch, infglob, infglobc, optglob, optglobc
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import warnings
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
# Simple utilities
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
def updateopt(options: dict, opt: dict) -> dict:
|
|
23
|
+
"""
|
|
24
|
+
Update an options dict with fields from *opt*, ignoring unknown keys.
|
|
25
|
+
|
|
26
|
+
Port of MATLAB ``updateopt``.
|
|
27
|
+
"""
|
|
28
|
+
if not isinstance(opt, dict):
|
|
29
|
+
return options
|
|
30
|
+
result = dict(options)
|
|
31
|
+
for k, v in opt.items():
|
|
32
|
+
if k in result:
|
|
33
|
+
result[k] = v
|
|
34
|
+
return result
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def uniproj(x):
|
|
38
|
+
"""
|
|
39
|
+
Project values onto [0, 1].
|
|
40
|
+
|
|
41
|
+
Port of MATLAB ``uniproj``.
|
|
42
|
+
"""
|
|
43
|
+
return np.clip(np.asarray(x, dtype=float), 0.0, 1.0)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def u2range(u, lo, hi):
|
|
47
|
+
"""
|
|
48
|
+
Linear mapping from [0, 1] onto [lo, hi].
|
|
49
|
+
|
|
50
|
+
Port of MATLAB ``u2range``.
|
|
51
|
+
"""
|
|
52
|
+
return float(lo) + (float(hi) - float(lo)) * float(u)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def randgamma(a, rows=1, cols=None):
|
|
56
|
+
"""
|
|
57
|
+
Gamma-distributed random numbers using Marsaglia-Tsang (2000).
|
|
58
|
+
|
|
59
|
+
Port of MATLAB ``randgamma``.
|
|
60
|
+
"""
|
|
61
|
+
if cols is None:
|
|
62
|
+
cols = rows
|
|
63
|
+
out = np.zeros((rows, cols))
|
|
64
|
+
if a == 0:
|
|
65
|
+
return out
|
|
66
|
+
if a < 1:
|
|
67
|
+
return randgamma(a + 1, rows, cols) * np.random.rand(rows, cols) ** (1.0 / a)
|
|
68
|
+
d = a - 1.0 / 3.0
|
|
69
|
+
c = 1.0 / np.sqrt(9.0 * d)
|
|
70
|
+
for idx in np.ndindex(rows, cols):
|
|
71
|
+
while True:
|
|
72
|
+
while True:
|
|
73
|
+
x = np.random.randn()
|
|
74
|
+
v = 1.0 + c * x
|
|
75
|
+
if v > 0:
|
|
76
|
+
break
|
|
77
|
+
v = v ** 3
|
|
78
|
+
u = np.random.rand()
|
|
79
|
+
if u < 1 - 0.0331 * x ** 4:
|
|
80
|
+
out[idx] = d * v
|
|
81
|
+
break
|
|
82
|
+
if np.log(u) < 0.5 * x ** 2 + d * (1 - v + np.log(v)):
|
|
83
|
+
out[idx] = d * v
|
|
84
|
+
break
|
|
85
|
+
return out
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def randbeta(a, b, rows=1, cols=None):
|
|
89
|
+
"""
|
|
90
|
+
Beta-distributed random numbers via ratio of Gamma samples.
|
|
91
|
+
|
|
92
|
+
Port of MATLAB ``randbeta``.
|
|
93
|
+
"""
|
|
94
|
+
if cols is None:
|
|
95
|
+
cols = rows
|
|
96
|
+
g1 = randgamma(a, rows, cols)
|
|
97
|
+
g2 = randgamma(b, rows, cols)
|
|
98
|
+
return g1 / (g1 + g2)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def sa_testfun(x):
|
|
102
|
+
"""
|
|
103
|
+
Complex test function with many local minima; global minimum f=0 at (0,0).
|
|
104
|
+
|
|
105
|
+
f(x,y) = x² + 2y² + 0.3(1−cos(3πx)) + 0.4(1−cos(4πy))
|
|
106
|
+
|
|
107
|
+
Port of MATLAB ``sa_testfun``.
|
|
108
|
+
"""
|
|
109
|
+
x = np.atleast_1d(np.asarray(x, dtype=float))
|
|
110
|
+
a, b = 1.0, 2.0
|
|
111
|
+
c, d_ = 0.3, 0.4
|
|
112
|
+
alpha, gamma = 3 * np.pi, 4 * np.pi
|
|
113
|
+
return (a * x[0] ** 2 + b * x[1] ** 2
|
|
114
|
+
+ c * (1 - np.cos(alpha * x[0]))
|
|
115
|
+
+ d_ * (1 - np.cos(gamma * x[1])))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
# Binary / Hilbert-curve encoding
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
def val2bin(x, nfrac: int = 0):
|
|
123
|
+
"""
|
|
124
|
+
Convert a non-negative real number to binary strings.
|
|
125
|
+
|
|
126
|
+
Returns
|
|
127
|
+
-------
|
|
128
|
+
bint : str — integer part in binary
|
|
129
|
+
bfrac : str — fractional part in binary (length = nfrac)
|
|
130
|
+
|
|
131
|
+
Port of MATLAB ``val2bin``.
|
|
132
|
+
"""
|
|
133
|
+
x = abs(float(x))
|
|
134
|
+
ix = int(x)
|
|
135
|
+
fx = x - ix
|
|
136
|
+
bint = bin(ix)[2:] or '0'
|
|
137
|
+
bfrac = ''
|
|
138
|
+
for _ in range(int(nfrac)):
|
|
139
|
+
fx *= 2
|
|
140
|
+
bit = int(fx)
|
|
141
|
+
fx -= bit
|
|
142
|
+
bfrac += str(bit)
|
|
143
|
+
return bint, bfrac
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def bin2val(bint: str, bfrac: str) -> float:
|
|
147
|
+
"""
|
|
148
|
+
Convert binary strings to a real number.
|
|
149
|
+
|
|
150
|
+
Port of MATLAB ``bin2val``.
|
|
151
|
+
"""
|
|
152
|
+
x = int(bint, 2) if bint else 0
|
|
153
|
+
if bfrac:
|
|
154
|
+
x += int(bfrac, 2) / (2 ** len(bfrac))
|
|
155
|
+
return float(x)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def coord2hilb(coord, precision: int) -> float:
|
|
159
|
+
"""
|
|
160
|
+
Map an N-D vector in [0,1]^N to a 1-D value in [0,1] using the Hilbert
|
|
161
|
+
space-filling curve.
|
|
162
|
+
|
|
163
|
+
Parameters
|
|
164
|
+
----------
|
|
165
|
+
coord : array-like, shape (N,) — coordinates in [0, 1]
|
|
166
|
+
precision : int — bits per coordinate
|
|
167
|
+
|
|
168
|
+
Returns
|
|
169
|
+
-------
|
|
170
|
+
x : float in [0, 1]
|
|
171
|
+
|
|
172
|
+
Port of MATLAB ``coord2hilb`` (Lawder 2000).
|
|
173
|
+
"""
|
|
174
|
+
coord = np.asarray(coord, dtype=float).ravel()
|
|
175
|
+
dimensions = len(coord)
|
|
176
|
+
p = int(precision)
|
|
177
|
+
|
|
178
|
+
# Build alpha matrix (p × d) — binary digits of each coordinate
|
|
179
|
+
alpha = np.zeros((p, dimensions), dtype=int)
|
|
180
|
+
for i in range(dimensions):
|
|
181
|
+
_, s = val2bin(coord[i], p)
|
|
182
|
+
for j in range(p):
|
|
183
|
+
alpha[j, i] = int(s[j]) if j < len(s) else 0
|
|
184
|
+
|
|
185
|
+
# Allocate arrays
|
|
186
|
+
omega = np.zeros((p, dimensions), dtype=int)
|
|
187
|
+
rho = np.zeros((p, dimensions), dtype=int)
|
|
188
|
+
sigma = np.zeros((p, dimensions), dtype=int)
|
|
189
|
+
tilde_sigma = np.zeros((p, dimensions), dtype=int)
|
|
190
|
+
tau = np.zeros((p, dimensions), dtype=int)
|
|
191
|
+
tilde_tau = np.zeros((p, dimensions), dtype=int)
|
|
192
|
+
J = np.zeros(p, dtype=int)
|
|
193
|
+
shift = np.zeros(p + 1, dtype=int)
|
|
194
|
+
|
|
195
|
+
for i in range(p):
|
|
196
|
+
if i > 0:
|
|
197
|
+
omega[i, :] = omega[i - 1, :] ^ tilde_tau[i - 1, :]
|
|
198
|
+
|
|
199
|
+
tilde_sigma[i, :] = alpha[i, :] ^ omega[i, :]
|
|
200
|
+
|
|
201
|
+
# Left-cyclic shift of tilde_sigma by shift[i]
|
|
202
|
+
k = shift[i]
|
|
203
|
+
sig = np.concatenate([tilde_sigma[i, :], tilde_sigma[i, :]])
|
|
204
|
+
sigma[i, :] = sig[k: k + dimensions]
|
|
205
|
+
|
|
206
|
+
# Gray-code-like: rho[0] = sigma[0]; rho[j] = sigma[j] ^ rho[j-1]
|
|
207
|
+
rho[i, 0] = sigma[i, 0]
|
|
208
|
+
for j in range(1, dimensions):
|
|
209
|
+
rho[i, j] = sigma[i, j] ^ rho[i, j - 1]
|
|
210
|
+
|
|
211
|
+
# Principal position J[i]: max index where rho != rho[-1]
|
|
212
|
+
rhoi = rho[i, :]
|
|
213
|
+
ind = [j for j in range(dimensions) if rhoi[j] != rhoi[-1]]
|
|
214
|
+
J[i] = max(ind) + 1 if ind else dimensions # 1-based
|
|
215
|
+
|
|
216
|
+
shift[i + 1] = (shift[i] + J[i] - 1) % dimensions
|
|
217
|
+
|
|
218
|
+
# tau: invert last bit; if odd parity, also invert J-th bit (1-based)
|
|
219
|
+
tau[i, :] = sigma[i, :]
|
|
220
|
+
tau[i, -1] ^= 1
|
|
221
|
+
if np.sum(tau[i, :]) % 2 == 1:
|
|
222
|
+
tau[i, J[i] - 1] ^= 1
|
|
223
|
+
|
|
224
|
+
# tilde_tau: right-cyclic shift of tau by shift[i]
|
|
225
|
+
k = dimensions - shift[i]
|
|
226
|
+
titau = np.concatenate([tau[i, :], tau[i, :]])
|
|
227
|
+
tilde_tau[i, :] = titau[k: k + dimensions]
|
|
228
|
+
|
|
229
|
+
# Flatten rho column-major (row by row in transposed sense) → binary string
|
|
230
|
+
r = rho.T.ravel()
|
|
231
|
+
r_str = ''.join(str(b) for b in r)
|
|
232
|
+
if len(r_str) > 52:
|
|
233
|
+
r_str = r_str[:52]
|
|
234
|
+
|
|
235
|
+
x = bin2val('0', r_str)
|
|
236
|
+
return x
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def hilb2coord(x: float, dimensions: int, precision: int) -> np.ndarray:
|
|
240
|
+
"""
|
|
241
|
+
Map a 1-D value in [0, 1] to an N-D vector using the Hilbert curve.
|
|
242
|
+
|
|
243
|
+
Parameters
|
|
244
|
+
----------
|
|
245
|
+
x : float in [0, 1]
|
|
246
|
+
dimensions : int — output dimensionality
|
|
247
|
+
precision : int — bits per output coordinate
|
|
248
|
+
|
|
249
|
+
Returns
|
|
250
|
+
-------
|
|
251
|
+
coord : np.ndarray, shape (dimensions,)
|
|
252
|
+
|
|
253
|
+
Port of MATLAB ``hilb2coord`` (Butz 1971, Lawder 2000).
|
|
254
|
+
"""
|
|
255
|
+
d = int(dimensions)
|
|
256
|
+
p = int(precision)
|
|
257
|
+
|
|
258
|
+
_, r_str = val2bin(float(x), d * p)
|
|
259
|
+
# Pad to length d*p
|
|
260
|
+
r_str = r_str.ljust(d * p, '0')[:d * p]
|
|
261
|
+
r = np.array([int(c) for c in r_str], dtype=int)
|
|
262
|
+
|
|
263
|
+
# rho: shape (p, d) — each row is one precision level
|
|
264
|
+
rho = r.reshape(p, d)
|
|
265
|
+
|
|
266
|
+
# J[i]: principal position in each row
|
|
267
|
+
J = np.zeros(p, dtype=int)
|
|
268
|
+
for i in range(p):
|
|
269
|
+
rhoi = rho[i, :]
|
|
270
|
+
ind = [j for j in range(d) if rhoi[j] != rhoi[-1]]
|
|
271
|
+
J[i] = max(ind) + 1 if ind else d # 1-based
|
|
272
|
+
|
|
273
|
+
# sigma: Gray-code of rho → sigma[i,j] = rho[i,j] ^ rho[i,j+1] (with rho[i,-1]=0)
|
|
274
|
+
sigma = np.zeros((p, d), dtype=int)
|
|
275
|
+
for i in range(p):
|
|
276
|
+
sig_row = np.concatenate([[rho[i, 0]], np.zeros(d, dtype=int)])
|
|
277
|
+
# sigma = rho XOR (rho shifted by 1)
|
|
278
|
+
sig_row = rho[i, :] ^ np.concatenate([[0], rho[i, :-1]])
|
|
279
|
+
sigma[i, :] = sig_row
|
|
280
|
+
|
|
281
|
+
# tau: complement last bit; if odd parity, complement J-th bit
|
|
282
|
+
tau = sigma.copy()
|
|
283
|
+
tau[:, -1] ^= 1
|
|
284
|
+
for i in range(p):
|
|
285
|
+
if np.sum(tau[i, :]) % 2 == 1:
|
|
286
|
+
tau[i, J[i] - 1] ^= 1
|
|
287
|
+
|
|
288
|
+
# shifts: shift[i] = sum_{k=0}^{i-1} (J[k]-1) mod d
|
|
289
|
+
shift_arr = np.zeros(p, dtype=int)
|
|
290
|
+
for i in range(1, p):
|
|
291
|
+
shift_arr[i] = (shift_arr[i - 1] + J[i - 1] - 1) % d
|
|
292
|
+
|
|
293
|
+
# tilde_sigma: right-cyclic shift of sigma[i] by shift[i]
|
|
294
|
+
tilde_sigma = np.zeros((p, d), dtype=int)
|
|
295
|
+
tilde_sigma[0, :] = sigma[0, :]
|
|
296
|
+
for i in range(1, p):
|
|
297
|
+
k = d - shift_arr[i]
|
|
298
|
+
ts = np.concatenate([sigma[i, :], sigma[i, :]])
|
|
299
|
+
tilde_sigma[i, :] = ts[k: k + d]
|
|
300
|
+
|
|
301
|
+
# tilde_tau: right-cyclic shift of tau[i] by shift[i]
|
|
302
|
+
tilde_tau = np.zeros((p, d), dtype=int)
|
|
303
|
+
tilde_tau[0, :] = tau[0, :]
|
|
304
|
+
for i in range(1, p):
|
|
305
|
+
k = d - shift_arr[i]
|
|
306
|
+
tt = np.concatenate([tau[i, :], tau[i, :]])
|
|
307
|
+
tilde_tau[i, :] = tt[k: k + d]
|
|
308
|
+
|
|
309
|
+
# omega[i] = omega[i-1] ^ tilde_tau[i-1]
|
|
310
|
+
omega = np.zeros((p, d), dtype=int)
|
|
311
|
+
for i in range(1, p):
|
|
312
|
+
omega[i, :] = omega[i - 1, :] ^ tilde_tau[i - 1, :]
|
|
313
|
+
|
|
314
|
+
# alpha[i] = omega[i] ^ tilde_sigma[i]
|
|
315
|
+
alpha = np.zeros((p, d), dtype=int)
|
|
316
|
+
for i in range(p):
|
|
317
|
+
alpha[i, :] = omega[i, :] ^ tilde_sigma[i, :]
|
|
318
|
+
|
|
319
|
+
# Convert alpha columns to real coordinates
|
|
320
|
+
coord = np.zeros(d)
|
|
321
|
+
for i in range(d):
|
|
322
|
+
s = ''.join(str(b) for b in alpha[:, i])
|
|
323
|
+
coord[i] = bin2val('0', s)
|
|
324
|
+
return coord
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
# ---------------------------------------------------------------------------
|
|
328
|
+
# Stability sector parameter mapping
|
|
329
|
+
# ---------------------------------------------------------------------------
|
|
330
|
+
|
|
331
|
+
def r2range(Ea: float, Eb: float, shifted: bool):
|
|
332
|
+
"""
|
|
333
|
+
Compute bounds for the free term r2 in a second-order factor (z² + r1·z + r2).
|
|
334
|
+
|
|
335
|
+
Parameters
|
|
336
|
+
----------
|
|
337
|
+
Ea : exp(−α·T)
|
|
338
|
+
Eb : exp(−π/β)
|
|
339
|
+
shifted : True for shifted sector, False for truncated
|
|
340
|
+
|
|
341
|
+
Returns
|
|
342
|
+
-------
|
|
343
|
+
r2min, r2max, E0
|
|
344
|
+
|
|
345
|
+
Port of MATLAB ``r2range``.
|
|
346
|
+
"""
|
|
347
|
+
r2max = Ea ** 2
|
|
348
|
+
if not shifted:
|
|
349
|
+
E0 = min(Ea, Eb)
|
|
350
|
+
r2min = -Ea * E0
|
|
351
|
+
else:
|
|
352
|
+
r2min = -(Ea ** 2) * Eb
|
|
353
|
+
E0 = Ea * Eb
|
|
354
|
+
return float(r2min), float(r2max), float(E0)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def r1range(r2: float, Ea: float, beta: float, shifted: bool):
|
|
358
|
+
"""
|
|
359
|
+
Compute bounds for r1 given r2 and sector parameters.
|
|
360
|
+
|
|
361
|
+
Port of MATLAB ``r1range``.
|
|
362
|
+
"""
|
|
363
|
+
Eb = np.exp(-np.pi / beta) if not np.isinf(beta) and beta > 0 else 0.0
|
|
364
|
+
E0 = Ea * Eb if shifted else min(Ea, Eb)
|
|
365
|
+
r2crit = E0 ** 2
|
|
366
|
+
r1min = -r2 / max(Ea, 1e-12) - Ea
|
|
367
|
+
if r2 <= r2crit:
|
|
368
|
+
r1max = r2 / max(E0, 1e-12) + E0
|
|
369
|
+
else:
|
|
370
|
+
sqr2 = np.sqrt(max(r2, 0.0))
|
|
371
|
+
if shifted:
|
|
372
|
+
sqr2 /= max(Ea, 1e-12)
|
|
373
|
+
if np.isinf(beta) or sqr2 < 1e-12:
|
|
374
|
+
r1max = 2.0 * np.sqrt(max(r2, 0.0))
|
|
375
|
+
else:
|
|
376
|
+
r1max = -2.0 * sqr2 * np.cos(-beta * np.log(sqr2))
|
|
377
|
+
return float(r1min), float(r1max), float(E0)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def admproj(p, alpha: float = 0.0, beta: float = np.inf,
|
|
381
|
+
dom: str = 's', T: float = 1.0):
|
|
382
|
+
"""
|
|
383
|
+
Project poles onto an admissible stability sector.
|
|
384
|
+
|
|
385
|
+
Parameters
|
|
386
|
+
----------
|
|
387
|
+
p : array-like — poles to project
|
|
388
|
+
alpha : degree of stability (≥ 0)
|
|
389
|
+
beta : oscillation limit (>0 truncated sector, <0 shifted sector)
|
|
390
|
+
dom : 's' (CT), 'z', or 'd'
|
|
391
|
+
T : sampling period (for 'z' or 'd')
|
|
392
|
+
|
|
393
|
+
Returns
|
|
394
|
+
-------
|
|
395
|
+
p_proj : np.ndarray — projected poles
|
|
396
|
+
|
|
397
|
+
Port of MATLAB ``admproj``.
|
|
398
|
+
"""
|
|
399
|
+
p = np.atleast_1d(np.array(p, dtype=complex)).ravel().copy()
|
|
400
|
+
shifted = bool(beta < 0)
|
|
401
|
+
beta = abs(float(beta))
|
|
402
|
+
|
|
403
|
+
p_zero = np.array([], dtype=complex)
|
|
404
|
+
if dom == 'z':
|
|
405
|
+
ind_zero = np.where(np.abs(p) < 1e-12)[0]
|
|
406
|
+
p_zero = np.zeros(len(ind_zero), dtype=complex)
|
|
407
|
+
p = np.delete(p, ind_zero)
|
|
408
|
+
|
|
409
|
+
if dom == 'z':
|
|
410
|
+
p = np.log(p) / T
|
|
411
|
+
elif dom == 'd':
|
|
412
|
+
p = -np.log(p) / T
|
|
413
|
+
|
|
414
|
+
p = p + (shifted * alpha)
|
|
415
|
+
alpha0 = (1 - int(shifted)) * alpha
|
|
416
|
+
|
|
417
|
+
for i in range(len(p)):
|
|
418
|
+
re = float(np.real(p[i]))
|
|
419
|
+
im = float(np.imag(p[i]))
|
|
420
|
+
re = min(re, -alpha0)
|
|
421
|
+
betaX = abs(im / re) if abs(re) > 1e-12 else np.inf
|
|
422
|
+
if betaX > beta and not np.isinf(beta):
|
|
423
|
+
if dom != 's' and abs(im - np.pi) < 1e-10:
|
|
424
|
+
re = -np.pi / beta
|
|
425
|
+
else:
|
|
426
|
+
im = im * beta / betaX
|
|
427
|
+
p[i] = re + 1j * im
|
|
428
|
+
|
|
429
|
+
p = p - (shifted * alpha)
|
|
430
|
+
|
|
431
|
+
if dom != 's':
|
|
432
|
+
ind_pi = np.where(np.abs(np.imag(p) - np.pi) < 1e-10)[0]
|
|
433
|
+
if dom == 'z':
|
|
434
|
+
p = np.exp(p * T)
|
|
435
|
+
elif dom == 'd':
|
|
436
|
+
p = np.exp(-p * T)
|
|
437
|
+
p[ind_pi] = np.real(p[ind_pi])
|
|
438
|
+
if dom == 'z':
|
|
439
|
+
p = np.concatenate([p, p_zero])
|
|
440
|
+
|
|
441
|
+
return p
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def par2cp(rho, alpha: float = 0.0, beta: float = np.inf, n_pairs: int = None):
|
|
445
|
+
"""
|
|
446
|
+
Map a vector of parameters rho ∈ [0,1]^n to the characteristic polynomial.
|
|
447
|
+
|
|
448
|
+
Parameters
|
|
449
|
+
----------
|
|
450
|
+
rho : array-like — parameters in [0, 1]
|
|
451
|
+
alpha : stability degree
|
|
452
|
+
beta : oscillation limit (>0 truncated, <0 shifted)
|
|
453
|
+
n_pairs : number of complex-conjugate pairs (default: len(rho)//2)
|
|
454
|
+
|
|
455
|
+
Returns
|
|
456
|
+
-------
|
|
457
|
+
Delta : np.ndarray — reciprocal of DeltaZ (for dioph input)
|
|
458
|
+
DeltaZ : np.ndarray — characteristic polynomial in z
|
|
459
|
+
poles : np.ndarray — closed-loop poles
|
|
460
|
+
|
|
461
|
+
Port of MATLAB ``par2cp``.
|
|
462
|
+
"""
|
|
463
|
+
rho = np.asarray(rho, dtype=float).ravel()
|
|
464
|
+
if n_pairs is None:
|
|
465
|
+
n_pairs = len(rho) // 2
|
|
466
|
+
|
|
467
|
+
shifted = bool(beta < 0)
|
|
468
|
+
beta_abs = abs(float(beta))
|
|
469
|
+
Ea = np.exp(-float(alpha))
|
|
470
|
+
Eb = np.exp(-np.pi / beta_abs) if not np.isinf(beta_abs) and beta_abs > 0 else 0.0
|
|
471
|
+
|
|
472
|
+
if not shifted and Eb > Ea and not np.isinf(beta_abs):
|
|
473
|
+
beta_abs = np.inf
|
|
474
|
+
Eb = 1.0
|
|
475
|
+
|
|
476
|
+
r2min, r2max, E0 = r2range(Ea, Eb, shifted)
|
|
477
|
+
|
|
478
|
+
poles = np.zeros(len(rho), dtype=complex)
|
|
479
|
+
DeltaZ = np.array([1.0])
|
|
480
|
+
k = 0
|
|
481
|
+
|
|
482
|
+
for _ in range(n_pairs):
|
|
483
|
+
r2 = r2min + np.clip(rho[k], 0.0, 1.0) * (r2max - r2min)
|
|
484
|
+
r1min, r1max, _ = r1range(r2, Ea, beta_abs, shifted)
|
|
485
|
+
r1 = r1min + np.clip(rho[k + 1], 0.0, 1.0) * (r1max - r1min)
|
|
486
|
+
d = np.array([1.0, r1, r2])
|
|
487
|
+
poles[k: k + 2] = np.roots(d)
|
|
488
|
+
DeltaZ = np.polymul(DeltaZ, d)
|
|
489
|
+
k += 2
|
|
490
|
+
|
|
491
|
+
r0min = -Ea
|
|
492
|
+
r0max = float(E0)
|
|
493
|
+
while k < len(rho):
|
|
494
|
+
r0 = r0min + np.clip(rho[k], 0.0, 1.0) * (r0max - r0min)
|
|
495
|
+
if abs(r0) < 1e-3:
|
|
496
|
+
r0 = 0.0
|
|
497
|
+
poles[k] = -r0
|
|
498
|
+
DeltaZ = np.polymul(DeltaZ, np.array([1.0, r0]))
|
|
499
|
+
k += 1
|
|
500
|
+
|
|
501
|
+
# Delta = reciprocal polynomial of DeltaZ (fliplr + strip leading zeros)
|
|
502
|
+
Delta = np.real(DeltaZ[::-1])
|
|
503
|
+
while len(Delta) > 1 and abs(Delta[0]) < 1e-14:
|
|
504
|
+
Delta = Delta[1:]
|
|
505
|
+
|
|
506
|
+
return Delta, DeltaZ, poles
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def cp2par(DeltaZ, alpha: float = 0.0, beta: float = np.inf, n_pairs: int = None):
|
|
510
|
+
"""
|
|
511
|
+
Inverse of par2cp: map a characteristic polynomial to [0,1]^n parameters.
|
|
512
|
+
|
|
513
|
+
Port of MATLAB ``cp2par``.
|
|
514
|
+
"""
|
|
515
|
+
DeltaZ = np.asarray(DeltaZ, dtype=float).ravel()
|
|
516
|
+
if n_pairs is None:
|
|
517
|
+
n_pairs = (len(DeltaZ) - 1) // 2
|
|
518
|
+
|
|
519
|
+
poles = np.roots(DeltaZ)
|
|
520
|
+
poles = admproj(poles, alpha, beta, 'z')
|
|
521
|
+
|
|
522
|
+
# Sort by |imag| descending (complex pairs first)
|
|
523
|
+
idx = np.argsort(np.abs(np.imag(poles)))[::-1]
|
|
524
|
+
poles = poles[idx]
|
|
525
|
+
|
|
526
|
+
shifted = bool(beta < 0)
|
|
527
|
+
beta_abs = abs(float(beta))
|
|
528
|
+
Ea = np.exp(-float(alpha))
|
|
529
|
+
Eb = np.exp(-np.pi / beta_abs) if not np.isinf(beta_abs) and beta_abs > 0 else 0.0
|
|
530
|
+
|
|
531
|
+
if not shifted and Eb > Ea and not np.isinf(beta_abs):
|
|
532
|
+
beta_abs = np.inf
|
|
533
|
+
Eb = 1.0
|
|
534
|
+
|
|
535
|
+
r2min, r2max, E0 = r2range(Ea, Eb, shifted)
|
|
536
|
+
|
|
537
|
+
rho = np.zeros(len(poles))
|
|
538
|
+
k = 0
|
|
539
|
+
|
|
540
|
+
for _ in range(n_pairs):
|
|
541
|
+
d = np.real(np.poly(poles[k: k + 2]))
|
|
542
|
+
r1, r2 = float(d[1]), float(d[2])
|
|
543
|
+
rho[k] = (r2 - r2min) / max(r2max - r2min, 1e-14)
|
|
544
|
+
r1min, r1max, _ = r1range(r2, Ea, beta_abs, shifted)
|
|
545
|
+
rho[k + 1] = (r1 - r1min) / max(r1max - r1min, 1e-14)
|
|
546
|
+
k += 2
|
|
547
|
+
|
|
548
|
+
r0min = -Ea
|
|
549
|
+
r0max = float(E0)
|
|
550
|
+
while k < len(poles):
|
|
551
|
+
r0 = float(-np.real(poles[k]))
|
|
552
|
+
rho[k] = (r0 - r0min) / max(r0max - r0min, 1e-14)
|
|
553
|
+
k += 1
|
|
554
|
+
|
|
555
|
+
return np.clip(rho, 0.0, 1.0)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def guesspoles(poles, n_poles: int) -> np.ndarray:
|
|
559
|
+
"""
|
|
560
|
+
Select N poles as an initial guess, preferring those with largest modulus.
|
|
561
|
+
|
|
562
|
+
Port of MATLAB ``guesspoles``.
|
|
563
|
+
"""
|
|
564
|
+
poles = np.atleast_1d(np.array(poles, dtype=complex))
|
|
565
|
+
idx = np.argsort(np.abs(poles))[::-1]
|
|
566
|
+
poles = list(poles[idx])
|
|
567
|
+
|
|
568
|
+
p = np.zeros(n_poles, dtype=complex)
|
|
569
|
+
i = 0
|
|
570
|
+
while i < n_poles and poles:
|
|
571
|
+
if abs(np.imag(poles[0])) > 1e-10:
|
|
572
|
+
if i < n_poles - 1:
|
|
573
|
+
p[i] = poles[0]
|
|
574
|
+
p[i + 1] = poles[1]
|
|
575
|
+
i += 2
|
|
576
|
+
poles = poles[2:]
|
|
577
|
+
else:
|
|
578
|
+
p[i] = poles[0]
|
|
579
|
+
poles.pop(0)
|
|
580
|
+
i += 1
|
|
581
|
+
|
|
582
|
+
return p
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
# ---------------------------------------------------------------------------
|
|
586
|
+
# k2ksi – recover the ksi polynomial from a given controller
|
|
587
|
+
# ---------------------------------------------------------------------------
|
|
588
|
+
|
|
589
|
+
def k2ksi(plant, K, dK0=None, T=None):
|
|
590
|
+
"""
|
|
591
|
+
Find the polynomial parameter ksi for a controller K.
|
|
592
|
+
|
|
593
|
+
The controller is parameterised as::
|
|
594
|
+
|
|
595
|
+
K(z) = (aDelta + d·ksi) / (bDelta − n·ksi)
|
|
596
|
+
|
|
597
|
+
where ``n/d = recip(Pz)`` (conjugate reciprocal of the discretised plant)
|
|
598
|
+
and ``aDelta, bDelta`` solve the Diophantine ``n·aDelta + dd·bDelta = Delta``.
|
|
599
|
+
|
|
600
|
+
Parameters
|
|
601
|
+
----------
|
|
602
|
+
plant : (num, den) tuple or scipy.signal.lti
|
|
603
|
+
Continuous-time SISO plant (P22 channel).
|
|
604
|
+
K : (num, den) tuple or scipy.signal.dlti
|
|
605
|
+
Discrete-time controller.
|
|
606
|
+
dK0 : array-like, optional
|
|
607
|
+
Mandatory factor in the controller denominator (default: [1.0]).
|
|
608
|
+
T : float, optional
|
|
609
|
+
Sampling period. Required when K is a tuple.
|
|
610
|
+
|
|
611
|
+
Returns
|
|
612
|
+
-------
|
|
613
|
+
ksi : np.ndarray — polynomial parameter (should be near-constant for
|
|
614
|
+
a controller obtained from par2cp/go_par2k)
|
|
615
|
+
aDelta : np.ndarray — Diophantine numerator solution
|
|
616
|
+
bDelta : np.ndarray — Diophantine denominator solution
|
|
617
|
+
|
|
618
|
+
Port of MATLAB ``k2ksi``.
|
|
619
|
+
"""
|
|
620
|
+
import scipy.signal as sig
|
|
621
|
+
from directsd.polynomial.transforms import dtfm
|
|
622
|
+
from directsd.polynomial.operations import striplz, recip
|
|
623
|
+
from directsd.polynomial.diophantine import dioph
|
|
624
|
+
|
|
625
|
+
# Extract T, K_num, K_den
|
|
626
|
+
if isinstance(K, sig.dlti):
|
|
627
|
+
T = K.dt
|
|
628
|
+
tf_k = K.to_tf()
|
|
629
|
+
K_num = np.atleast_1d(np.array(tf_k.num, float)).ravel()
|
|
630
|
+
K_den = np.atleast_1d(np.array(tf_k.den, float)).ravel()
|
|
631
|
+
elif isinstance(K, tuple):
|
|
632
|
+
if T is None:
|
|
633
|
+
raise ValueError("T must be provided when K is a (num, den) tuple")
|
|
634
|
+
K_num = np.atleast_1d(np.array(K[0], float)).ravel()
|
|
635
|
+
K_den = np.atleast_1d(np.array(K[1], float)).ravel()
|
|
636
|
+
else:
|
|
637
|
+
raise TypeError(f"Unsupported controller type: {type(K)}")
|
|
638
|
+
|
|
639
|
+
if dK0 is None:
|
|
640
|
+
dK0 = np.array([1.0])
|
|
641
|
+
else:
|
|
642
|
+
dK0 = np.atleast_1d(np.array(dK0, float)).ravel()
|
|
643
|
+
|
|
644
|
+
# Discretise the plant
|
|
645
|
+
if isinstance(plant, sig.lti):
|
|
646
|
+
P_num = np.atleast_1d(np.array(plant.num, float)).ravel()
|
|
647
|
+
P_den = np.atleast_1d(np.array(plant.den, float)).ravel()
|
|
648
|
+
elif isinstance(plant, tuple):
|
|
649
|
+
P_num = np.atleast_1d(np.array(plant[0], float)).ravel()
|
|
650
|
+
P_den = np.atleast_1d(np.array(plant[1], float)).ravel()
|
|
651
|
+
else:
|
|
652
|
+
raise TypeError(f"Unsupported plant type: {type(plant)}")
|
|
653
|
+
|
|
654
|
+
D22_num, D22_den = dtfm((P_num, P_den), T)
|
|
655
|
+
D22_num = np.real(np.asarray(D22_num, float)).ravel()
|
|
656
|
+
D22_den = np.real(np.asarray(D22_den, float)).ravel()
|
|
657
|
+
|
|
658
|
+
# Characteristic polynomial DeltaZ = D22_den*K_den + D22_num*K_num
|
|
659
|
+
DeltaZ = np.polyadd(np.polymul(D22_den, K_den), np.polymul(D22_num, K_num))
|
|
660
|
+
DeltaZ = np.real(DeltaZ / DeltaZ[0]) # monic
|
|
661
|
+
|
|
662
|
+
# Delta = recip(DeltaZ)
|
|
663
|
+
Delta = np.real(DeltaZ[::-1])
|
|
664
|
+
Delta = striplz(Delta) if len(Delta) > 1 else Delta
|
|
665
|
+
|
|
666
|
+
# Pz' = conjugate reciprocal of Pz → for real coefficients: recip(Pz)
|
|
667
|
+
n = striplz(D22_num[::-1]) # recip of D22_num
|
|
668
|
+
d = striplz(D22_den[::-1]) # recip of D22_den
|
|
669
|
+
|
|
670
|
+
# dd = d * dK0
|
|
671
|
+
dd = np.polymul(d, dK0)
|
|
672
|
+
|
|
673
|
+
# Diophantine: n*aDelta + dd*bDelta = Delta
|
|
674
|
+
if len(n) - 1 <= len(d) - 1:
|
|
675
|
+
X, Y, _, _ = dioph(n, dd, Delta)
|
|
676
|
+
aDelta = np.real(np.asarray(
|
|
677
|
+
X.coef if hasattr(X, 'coef') else X, float)).ravel()
|
|
678
|
+
bDelta = np.real(np.asarray(
|
|
679
|
+
Y.coef if hasattr(Y, 'coef') else Y, float)).ravel()
|
|
680
|
+
else:
|
|
681
|
+
X, Y, _, _ = dioph(dd, n, Delta)
|
|
682
|
+
bDelta = np.real(np.asarray(
|
|
683
|
+
X.coef if hasattr(X, 'coef') else X, float)).ravel()
|
|
684
|
+
aDelta = np.real(np.asarray(
|
|
685
|
+
Y.coef if hasattr(Y, 'coef') else Y, float)).ravel()
|
|
686
|
+
|
|
687
|
+
aDelta = striplz(aDelta) if len(aDelta) > 1 else aDelta
|
|
688
|
+
bDelta = striplz(bDelta) if len(bDelta) > 1 else bDelta
|
|
689
|
+
|
|
690
|
+
# K' = recip(K) for real DT polynomial
|
|
691
|
+
Kr_num = K_num[::-1] # recip of K numerator
|
|
692
|
+
Kr_den = K_den[::-1] # recip of K denominator
|
|
693
|
+
|
|
694
|
+
# ksiN = K'*bDelta - aDelta (as TF numerator / denominator)
|
|
695
|
+
# K'*bDelta → (polymul(Kr_num, bDelta), Kr_den)
|
|
696
|
+
# -aDelta → (-aDelta, [1.0])
|
|
697
|
+
# sum → (polymul(Kr_num,bDelta)*1 + (-aDelta)*Kr_den, Kr_den)
|
|
698
|
+
ksiN_num = np.polyadd(np.polymul(Kr_num, bDelta),
|
|
699
|
+
np.polymul(-aDelta, Kr_den))
|
|
700
|
+
ksiN_den = Kr_den
|
|
701
|
+
|
|
702
|
+
# ksiD = K'*n + d (as TF)
|
|
703
|
+
# K'*n → (polymul(Kr_num, n), Kr_den)
|
|
704
|
+
# d → (d, [1.0])
|
|
705
|
+
# sum → (polymul(Kr_num,n) + d*Kr_den, Kr_den)
|
|
706
|
+
ksiD_num = np.polyadd(np.polymul(Kr_num, n),
|
|
707
|
+
np.polymul(d, Kr_den))
|
|
708
|
+
ksiD_den = Kr_den
|
|
709
|
+
|
|
710
|
+
# ksi = ksiN / ksiD → (ksiN_num * ksiD_den) / (ksiN_den * ksiD_num)
|
|
711
|
+
ksi_num = np.polymul(ksiN_num, ksiD_den)
|
|
712
|
+
ksi_den = np.polymul(ksiN_den, ksiD_num)
|
|
713
|
+
|
|
714
|
+
# Simplify by GCD
|
|
715
|
+
from directsd.polynomial.operations import gcd
|
|
716
|
+
g = gcd(ksi_num, ksi_den)
|
|
717
|
+
g_coef = np.asarray(g.coef if hasattr(g, 'coef') else g, float).ravel()
|
|
718
|
+
if len(g_coef) > 1:
|
|
719
|
+
ksi_num = np.real(np.polydiv(ksi_num, g_coef)[0])
|
|
720
|
+
ksi_den = np.real(np.polydiv(ksi_den, g_coef)[0])
|
|
721
|
+
|
|
722
|
+
ksi_num = striplz(np.real(ksi_num))
|
|
723
|
+
ksi_den = striplz(np.real(ksi_den))
|
|
724
|
+
|
|
725
|
+
# Polynomial long division: ksi should be a polynomial (near-zero remainder)
|
|
726
|
+
ksi, err = np.polydiv(ksi_num, ksi_den)
|
|
727
|
+
ksi = np.real(ksi)
|
|
728
|
+
|
|
729
|
+
err_norm = np.linalg.norm(err)
|
|
730
|
+
den_norm = np.linalg.norm(ksi_den)
|
|
731
|
+
if err_norm > 1e-3 * den_norm:
|
|
732
|
+
import warnings
|
|
733
|
+
warnings.warn(
|
|
734
|
+
f"k2ksi: cancellation error {err_norm:.4g} (den norm {den_norm:.4g}). "
|
|
735
|
+
"ksi may not be a pure polynomial.",
|
|
736
|
+
RuntimeWarning, stacklevel=2,
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
return ksi, aDelta, bDelta
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
# ---------------------------------------------------------------------------
|
|
743
|
+
# Controller-parameter target functions (closures, no global state)
|
|
744
|
+
# ---------------------------------------------------------------------------
|
|
745
|
+
|
|
746
|
+
def go_par2k(coef, ctx: dict):
|
|
747
|
+
"""
|
|
748
|
+
Map a parameter vector coef ∈ [0,1]^n to a discrete controller.
|
|
749
|
+
|
|
750
|
+
Parameters
|
|
751
|
+
----------
|
|
752
|
+
coef : array-like — parameters in [0, 1]
|
|
753
|
+
ctx : dict with keys:
|
|
754
|
+
alpha, beta, n_pairs, D22_num, D22_den
|
|
755
|
+
(and optionally dK0 for forced denominator factor)
|
|
756
|
+
|
|
757
|
+
Returns
|
|
758
|
+
-------
|
|
759
|
+
K_num, K_den : np.ndarray — controller numerator/denominator
|
|
760
|
+
|
|
761
|
+
Simplified port of MATLAB ``go_par2k`` (ksi=0 path).
|
|
762
|
+
"""
|
|
763
|
+
from directsd.polynomial.operations import striplz
|
|
764
|
+
from directsd.polynomial.diophantine import dioph
|
|
765
|
+
|
|
766
|
+
coef = np.asarray(coef, dtype=float).ravel()
|
|
767
|
+
alpha = float(ctx.get('alpha', 0.0))
|
|
768
|
+
beta = float(ctx.get('beta', np.inf))
|
|
769
|
+
n_pairs = ctx.get('n_pairs', len(coef) // 2)
|
|
770
|
+
|
|
771
|
+
Delta, DeltaZ, _ = par2cp(coef, alpha, beta, n_pairs)
|
|
772
|
+
|
|
773
|
+
D22_num = np.asarray(ctx['D22_num'], dtype=float).ravel()
|
|
774
|
+
D22_den = np.asarray(ctx['D22_den'], dtype=float).ravel()
|
|
775
|
+
dK0 = np.asarray(ctx.get('dK0', [1.0]), dtype=float).ravel()
|
|
776
|
+
|
|
777
|
+
dd = np.polymul(D22_den, dK0)
|
|
778
|
+
n, d = D22_num, dd
|
|
779
|
+
|
|
780
|
+
if len(n) <= len(d):
|
|
781
|
+
X, Y, _, _ = dioph(n, d, Delta)
|
|
782
|
+
aDelta = np.asarray(X.coef if hasattr(X, 'coef') else X, float).ravel()
|
|
783
|
+
bDelta = np.asarray(Y.coef if hasattr(Y, 'coef') else Y, float).ravel()
|
|
784
|
+
else:
|
|
785
|
+
X, Y, _, _ = dioph(d, n, Delta)
|
|
786
|
+
bDelta = np.asarray(X.coef if hasattr(X, 'coef') else X, float).ravel()
|
|
787
|
+
aDelta = np.asarray(Y.coef if hasattr(Y, 'coef') else Y, float).ravel()
|
|
788
|
+
|
|
789
|
+
ksi = np.array([0.0])
|
|
790
|
+
K_num = np.polyadd(aDelta, np.polymul(np.polymul(D22_den, ksi), dK0))
|
|
791
|
+
K_den = np.polymul(np.polyadd(bDelta, -np.polymul(D22_num, ksi)), dK0)
|
|
792
|
+
|
|
793
|
+
K_num = np.real(striplz(K_num))
|
|
794
|
+
K_den = np.real(striplz(K_den))
|
|
795
|
+
return K_num, K_den
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def f_sdh2p(coef, ctx: dict):
|
|
799
|
+
"""
|
|
800
|
+
H2-norm target function for modal optimization.
|
|
801
|
+
|
|
802
|
+
Parameters
|
|
803
|
+
----------
|
|
804
|
+
coef : array-like — parameters in [0, 1]; or (K_num, K_den) tuple
|
|
805
|
+
ctx : dict with keys: plant, T, alpha, beta, n_pairs, D22_num, D22_den
|
|
806
|
+
|
|
807
|
+
Returns
|
|
808
|
+
-------
|
|
809
|
+
f : float — H2-norm value
|
|
810
|
+
K : (K_num, K_den) tuple
|
|
811
|
+
|
|
812
|
+
Port of MATLAB ``f_sdh2p``.
|
|
813
|
+
"""
|
|
814
|
+
from directsd.analysis.norms import sdh2norm
|
|
815
|
+
|
|
816
|
+
if isinstance(coef, tuple) and len(coef) == 2:
|
|
817
|
+
K = coef
|
|
818
|
+
else:
|
|
819
|
+
K = go_par2k(coef, ctx)
|
|
820
|
+
|
|
821
|
+
plant = ctx['plant']
|
|
822
|
+
T = float(ctx['T'])
|
|
823
|
+
try:
|
|
824
|
+
f = float(sdh2norm(plant, K, T))
|
|
825
|
+
if not np.isfinite(f):
|
|
826
|
+
f = float('inf')
|
|
827
|
+
except Exception:
|
|
828
|
+
f = float('inf')
|
|
829
|
+
return f, K
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
def f_sdl2p(coef, ctx: dict):
|
|
833
|
+
"""
|
|
834
|
+
L2-error target function for modal optimization.
|
|
835
|
+
|
|
836
|
+
Parameters
|
|
837
|
+
----------
|
|
838
|
+
coef : array-like — parameters in [0, 1]; or (K_num, K_den) tuple
|
|
839
|
+
ctx : dict with keys: plant, T, alpha, beta, n_pairs, D22_num, D22_den
|
|
840
|
+
|
|
841
|
+
Returns
|
|
842
|
+
-------
|
|
843
|
+
f : float — L2-error value
|
|
844
|
+
K : (K_num, K_den) tuple
|
|
845
|
+
|
|
846
|
+
Port of MATLAB ``f_sdl2p``.
|
|
847
|
+
"""
|
|
848
|
+
from directsd.analysis.errors import sdl2err
|
|
849
|
+
|
|
850
|
+
if isinstance(coef, tuple) and len(coef) == 2:
|
|
851
|
+
K = coef
|
|
852
|
+
else:
|
|
853
|
+
K = go_par2k(coef, ctx)
|
|
854
|
+
|
|
855
|
+
plant = ctx['plant']
|
|
856
|
+
T = float(ctx['T'])
|
|
857
|
+
try:
|
|
858
|
+
f = float(sdl2err(plant, K, T))
|
|
859
|
+
if not np.isfinite(f):
|
|
860
|
+
f = float('inf')
|
|
861
|
+
except Exception:
|
|
862
|
+
f = float('inf')
|
|
863
|
+
return f, K
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def go_sdh2p(x: float, ctx: dict):
|
|
867
|
+
"""
|
|
868
|
+
H2 target for modal Hilbert-curve optimization.
|
|
869
|
+
|
|
870
|
+
Parameters
|
|
871
|
+
----------
|
|
872
|
+
x : float in [0, 1] — Hilbert curve parameter
|
|
873
|
+
ctx : dict with keys as for f_sdh2p, plus 'dim' and 'bits'
|
|
874
|
+
|
|
875
|
+
Returns
|
|
876
|
+
-------
|
|
877
|
+
f, coef, K
|
|
878
|
+
|
|
879
|
+
Port of MATLAB ``go_sdh2p``.
|
|
880
|
+
"""
|
|
881
|
+
dim = int(ctx['dim'])
|
|
882
|
+
bits = int(ctx.get('bits', 8))
|
|
883
|
+
coef = hilb2coord(float(x), dim, bits)
|
|
884
|
+
f, K = f_sdh2p(coef, ctx)
|
|
885
|
+
return f, coef, K
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
def go_sdl2p(x: float, ctx: dict):
|
|
889
|
+
"""
|
|
890
|
+
L2 target for modal Hilbert-curve optimization.
|
|
891
|
+
|
|
892
|
+
Port of MATLAB ``go_sdl2p``.
|
|
893
|
+
"""
|
|
894
|
+
dim = int(ctx['dim'])
|
|
895
|
+
bits = int(ctx.get('bits', 8))
|
|
896
|
+
coef = hilb2coord(float(x), dim, bits)
|
|
897
|
+
f, K = f_sdl2p(coef, ctx)
|
|
898
|
+
return f, coef, K
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
# ---------------------------------------------------------------------------
|
|
902
|
+
# Sasimplex – simulated annealing with Nelder-Mead
|
|
903
|
+
# ---------------------------------------------------------------------------
|
|
904
|
+
|
|
905
|
+
def sasimplex(func, x0, options=None):
|
|
906
|
+
"""
|
|
907
|
+
Simulated annealing using the Nelder-Mead simplex method.
|
|
908
|
+
|
|
909
|
+
Parameters
|
|
910
|
+
----------
|
|
911
|
+
func : callable — f(x) -> float
|
|
912
|
+
x0 : array-like — initial guess
|
|
913
|
+
options : dict, optional
|
|
914
|
+
display, tol, maxFunEvals, dispIter, multiStep,
|
|
915
|
+
startTemp, tempDecRate
|
|
916
|
+
|
|
917
|
+
Returns
|
|
918
|
+
-------
|
|
919
|
+
x_best : np.ndarray
|
|
920
|
+
y_best : float
|
|
921
|
+
n_evals : int
|
|
922
|
+
|
|
923
|
+
Port of MATLAB ``sasimplex``.
|
|
924
|
+
"""
|
|
925
|
+
defaults = dict(
|
|
926
|
+
display='off',
|
|
927
|
+
tol=1e-5,
|
|
928
|
+
maxFunEvals=10000,
|
|
929
|
+
dispIter=100,
|
|
930
|
+
multiStep=20,
|
|
931
|
+
startTemp=100.0,
|
|
932
|
+
tempDecRate=None,
|
|
933
|
+
)
|
|
934
|
+
if options:
|
|
935
|
+
defaults = updateopt(defaults, options)
|
|
936
|
+
opt = defaults
|
|
937
|
+
|
|
938
|
+
display = opt['display']
|
|
939
|
+
tol = float(opt['tol'])
|
|
940
|
+
max_feval = int(opt['maxFunEvals'])
|
|
941
|
+
disp_iter = int(opt['dispIter'])
|
|
942
|
+
M = int(opt['multiStep'])
|
|
943
|
+
T0 = -float(opt['startTemp'])
|
|
944
|
+
alpha_decay = opt['tempDecRate']
|
|
945
|
+
if alpha_decay is None:
|
|
946
|
+
alpha_decay = -np.log(0.1) / 200.0
|
|
947
|
+
|
|
948
|
+
verbosity = {'off': 0, 'final': 1, 'on': 2}.get(display, 0)
|
|
949
|
+
|
|
950
|
+
x0 = np.asarray(x0, dtype=float).ravel()
|
|
951
|
+
n = len(x0)
|
|
952
|
+
rho, chi, psi, sigma = 1.0, 2.0, 0.5, 0.5
|
|
953
|
+
|
|
954
|
+
# Build initial simplex
|
|
955
|
+
X = np.zeros((n, n + 1))
|
|
956
|
+
y = np.zeros(n + 1)
|
|
957
|
+
X[:, 0] = x0
|
|
958
|
+
y[0] = func(x0)
|
|
959
|
+
n_evals = 1
|
|
960
|
+
|
|
961
|
+
for j in range(n):
|
|
962
|
+
z = x0.copy()
|
|
963
|
+
z[j] = z[j] * 1.05 if z[j] != 0 else 0.00025
|
|
964
|
+
X[:, j + 1] = z
|
|
965
|
+
y[j + 1] = func(z) # fixed MATLAB bug: was feval(func, y)
|
|
966
|
+
n_evals += 1
|
|
967
|
+
|
|
968
|
+
x_best = x0.copy()
|
|
969
|
+
y_best = y[0]
|
|
970
|
+
T = T0
|
|
971
|
+
iteration = 0
|
|
972
|
+
|
|
973
|
+
while np.max(np.abs(X[:, 1:] - X[:, :1])) > tol:
|
|
974
|
+
# Stochastic fluctuation for all but best
|
|
975
|
+
order = np.argsort(y)
|
|
976
|
+
y = y[order]; X = X[:, order]
|
|
977
|
+
y_flu = np.concatenate([[y[0]],
|
|
978
|
+
y[1:] + T * np.log(np.random.rand(n) + 1e-300)])
|
|
979
|
+
|
|
980
|
+
order2 = np.argsort(y_flu)
|
|
981
|
+
y = y[order2]; X = X[:, order2]; y_flu = y_flu[order2]
|
|
982
|
+
|
|
983
|
+
xbar = X[:, :n].mean(axis=1)
|
|
984
|
+
|
|
985
|
+
# Reflection
|
|
986
|
+
xr = (1 + rho) * xbar - rho * X[:, n]
|
|
987
|
+
fr = func(xr)
|
|
988
|
+
fr_flu = fr - T * np.log(np.random.rand() + 1e-300)
|
|
989
|
+
n_evals += 1
|
|
990
|
+
|
|
991
|
+
if fr_flu < y_flu[0]:
|
|
992
|
+
# Try expansion
|
|
993
|
+
xe = (1 + rho * chi) * xbar - rho * chi * X[:, n]
|
|
994
|
+
fe = func(xe)
|
|
995
|
+
fe_flu = fe - T * np.log(np.random.rand() + 1e-300)
|
|
996
|
+
n_evals += 1
|
|
997
|
+
if fe_flu < fr_flu:
|
|
998
|
+
X[:, n] = xe; y[n] = fe
|
|
999
|
+
else:
|
|
1000
|
+
X[:, n] = xr; y[n] = fr
|
|
1001
|
+
elif fr_flu < y_flu[n - 1]:
|
|
1002
|
+
X[:, n] = xr; y[n] = fr
|
|
1003
|
+
else:
|
|
1004
|
+
# Contraction
|
|
1005
|
+
if fr_flu < y_flu[n]:
|
|
1006
|
+
xc = (1 + psi * rho) * xbar - psi * rho * X[:, n]
|
|
1007
|
+
else:
|
|
1008
|
+
xc = (1 - psi) * xbar + psi * X[:, n]
|
|
1009
|
+
fc = func(xc)
|
|
1010
|
+
fc_flu = fc - T * np.log(np.random.rand() + 1e-300)
|
|
1011
|
+
n_evals += 1
|
|
1012
|
+
if fc_flu <= min(fr_flu, y_flu[n]):
|
|
1013
|
+
X[:, n] = xc; y[n] = fc
|
|
1014
|
+
else:
|
|
1015
|
+
for j in range(1, n + 1):
|
|
1016
|
+
X[:, j] = X[:, 0] + sigma * (X[:, j] - X[:, 0])
|
|
1017
|
+
y[j] = func(X[:, j])
|
|
1018
|
+
n_evals += n
|
|
1019
|
+
|
|
1020
|
+
yi_min = np.min(y)
|
|
1021
|
+
if yi_min < y_best:
|
|
1022
|
+
y_best = yi_min
|
|
1023
|
+
x_best = X[:, np.argmin(y)].copy()
|
|
1024
|
+
|
|
1025
|
+
iteration += 1
|
|
1026
|
+
if n_evals >= max_feval:
|
|
1027
|
+
break
|
|
1028
|
+
|
|
1029
|
+
T = T0 * np.exp(-alpha_decay * iteration)
|
|
1030
|
+
|
|
1031
|
+
if verbosity > 1 and iteration % disp_iter == 0:
|
|
1032
|
+
print(f" sasimplex iter {iteration}: y_best={y_best:.5g}, T={T:.5g}")
|
|
1033
|
+
|
|
1034
|
+
if verbosity > 0:
|
|
1035
|
+
print(f"sasimplex done: y_best={y_best:.5g}, evals={n_evals}")
|
|
1036
|
+
|
|
1037
|
+
return x_best, y_best, n_evals
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
# ---------------------------------------------------------------------------
|
|
1041
|
+
# Arandsearch – accelerated random search
|
|
1042
|
+
# ---------------------------------------------------------------------------
|
|
1043
|
+
|
|
1044
|
+
def arandsearch(func, x0, options=None, constraint_func=None, proj_func=None):
|
|
1045
|
+
"""
|
|
1046
|
+
Accelerated random search optimization (Appel et al. 2004).
|
|
1047
|
+
|
|
1048
|
+
Parameters
|
|
1049
|
+
----------
|
|
1050
|
+
func : callable — f(x) -> float
|
|
1051
|
+
x0 : array-like — initial guess
|
|
1052
|
+
options : dict, optional
|
|
1053
|
+
constraint_func : callable, optional — g(x) -> array; <=0 is feasible
|
|
1054
|
+
proj_func : callable, optional — proj(x) -> x_projected
|
|
1055
|
+
|
|
1056
|
+
Returns
|
|
1057
|
+
-------
|
|
1058
|
+
x_best : np.ndarray
|
|
1059
|
+
val_best : float
|
|
1060
|
+
n_evals : int
|
|
1061
|
+
|
|
1062
|
+
Port of MATLAB ``arandsearch``.
|
|
1063
|
+
"""
|
|
1064
|
+
defaults = dict(
|
|
1065
|
+
display='off',
|
|
1066
|
+
tol=1e-4,
|
|
1067
|
+
maxFunEvals=10000,
|
|
1068
|
+
dispIter=100,
|
|
1069
|
+
iniStep=0.1,
|
|
1070
|
+
multiStep=1,
|
|
1071
|
+
maxFail=10,
|
|
1072
|
+
decStepBy=0.5,
|
|
1073
|
+
maxSuccess=2,
|
|
1074
|
+
incStepBy=2.0,
|
|
1075
|
+
adaptRate=0.1,
|
|
1076
|
+
)
|
|
1077
|
+
if options:
|
|
1078
|
+
defaults = updateopt(defaults, options)
|
|
1079
|
+
opt = defaults
|
|
1080
|
+
|
|
1081
|
+
verbosity = {'off': 0, 'final': 1, 'on': 2}.get(opt['display'], 0)
|
|
1082
|
+
|
|
1083
|
+
x = np.asarray(x0, dtype=float).ravel().copy()
|
|
1084
|
+
n = len(x)
|
|
1085
|
+
|
|
1086
|
+
if proj_func:
|
|
1087
|
+
x = proj_func(x)
|
|
1088
|
+
|
|
1089
|
+
val_best = func(x)
|
|
1090
|
+
x_best = x.copy()
|
|
1091
|
+
n_evals = 1
|
|
1092
|
+
|
|
1093
|
+
step = float(opt['iniStep'])
|
|
1094
|
+
h = step
|
|
1095
|
+
multi_step = int(opt['multiStep'])
|
|
1096
|
+
max_fail = int(opt['maxFail'])
|
|
1097
|
+
dec_step = float(opt['decStepBy'])
|
|
1098
|
+
max_success = int(opt['maxSuccess'])
|
|
1099
|
+
inc_step = float(opt['incStepBy'])
|
|
1100
|
+
adapt_rate = float(opt['adaptRate'])
|
|
1101
|
+
max_feval = int(opt['maxFunEvals'])
|
|
1102
|
+
disp_iter = int(opt['dispIter'])
|
|
1103
|
+
|
|
1104
|
+
fail_count = 0
|
|
1105
|
+
success_count = 0
|
|
1106
|
+
adapt_dir = np.zeros(n)
|
|
1107
|
+
iteration = 0
|
|
1108
|
+
|
|
1109
|
+
while step > opt['tol']:
|
|
1110
|
+
iteration += 1
|
|
1111
|
+
if n_evals >= max_feval:
|
|
1112
|
+
break
|
|
1113
|
+
|
|
1114
|
+
val_min = np.inf
|
|
1115
|
+
dx_best = np.zeros(n)
|
|
1116
|
+
for _ in range(multi_step):
|
|
1117
|
+
direction = adapt_dir + np.random.rand(n) - 0.5
|
|
1118
|
+
norm_d = np.linalg.norm(direction)
|
|
1119
|
+
if norm_d > 1e-14:
|
|
1120
|
+
direction /= norm_d
|
|
1121
|
+
dx = direction * h
|
|
1122
|
+
x_new = x + dx
|
|
1123
|
+
if proj_func:
|
|
1124
|
+
x_new = proj_func(x_new)
|
|
1125
|
+
if constraint_func and np.any(np.asarray(constraint_func(x_new)) > 0):
|
|
1126
|
+
continue
|
|
1127
|
+
val_new = func(x_new)
|
|
1128
|
+
n_evals += 1
|
|
1129
|
+
if val_new < val_min:
|
|
1130
|
+
val_min = val_new
|
|
1131
|
+
dx_best = x_new - x
|
|
1132
|
+
|
|
1133
|
+
if not np.isinf(val_min):
|
|
1134
|
+
if val_min > -np.inf:
|
|
1135
|
+
adapt_dir = ((1 - adapt_rate) * adapt_dir
|
|
1136
|
+
+ adapt_rate * dx_best * np.sign(val_best - val_min))
|
|
1137
|
+
norm_ad = np.linalg.norm(adapt_dir)
|
|
1138
|
+
if norm_ad > 1e-14:
|
|
1139
|
+
adapt_dir = adapt_rate * adapt_dir / norm_ad
|
|
1140
|
+
|
|
1141
|
+
if val_min < val_best:
|
|
1142
|
+
val_best = val_min
|
|
1143
|
+
x = x + dx_best
|
|
1144
|
+
x_best = x.copy()
|
|
1145
|
+
success_count += 1
|
|
1146
|
+
fail_count = 0
|
|
1147
|
+
h = step
|
|
1148
|
+
if success_count >= max_success:
|
|
1149
|
+
step *= inc_step
|
|
1150
|
+
success_count = 0
|
|
1151
|
+
if verbosity > 1:
|
|
1152
|
+
print(f" arandsearch iter {iteration}: "
|
|
1153
|
+
f"val={val_best:.5g} step={step:.4g} ^^")
|
|
1154
|
+
else:
|
|
1155
|
+
success_count = 0
|
|
1156
|
+
h *= dec_step
|
|
1157
|
+
if h < opt['tol']:
|
|
1158
|
+
fail_count += 1
|
|
1159
|
+
if fail_count >= max_fail:
|
|
1160
|
+
step *= dec_step
|
|
1161
|
+
fail_count = 0
|
|
1162
|
+
if verbosity > 1:
|
|
1163
|
+
print(f" arandsearch iter {iteration}: "
|
|
1164
|
+
f"val={val_best:.5g} step={step:.4g} vv")
|
|
1165
|
+
h = step
|
|
1166
|
+
|
|
1167
|
+
if verbosity > 1 and iteration % disp_iter == 0:
|
|
1168
|
+
print(f" arandsearch iter {iteration}: val={val_best:.5g} step={step:.4g}")
|
|
1169
|
+
|
|
1170
|
+
if verbosity > 0:
|
|
1171
|
+
if n_evals >= max_feval:
|
|
1172
|
+
print(f"arandsearch: not converged in {max_feval} evals.")
|
|
1173
|
+
else:
|
|
1174
|
+
print("arandsearch: converged.")
|
|
1175
|
+
|
|
1176
|
+
return x_best, val_best, n_evals
|
|
1177
|
+
|
|
1178
|
+
|
|
1179
|
+
# ---------------------------------------------------------------------------
|
|
1180
|
+
# infglob – information algorithm (1-D Lipschitz optimizer)
|
|
1181
|
+
# ---------------------------------------------------------------------------
|
|
1182
|
+
|
|
1183
|
+
def infglob(func, options=None):
|
|
1184
|
+
"""
|
|
1185
|
+
Global optimization of a scalar function on [0, 1] using Strongin's
|
|
1186
|
+
information algorithm.
|
|
1187
|
+
|
|
1188
|
+
Parameters
|
|
1189
|
+
----------
|
|
1190
|
+
func : callable — f(x) -> float, x ∈ [0, 1]
|
|
1191
|
+
options : dict, optional
|
|
1192
|
+
display, tol, maxIter, dispIter, dim, r, ksi, guess
|
|
1193
|
+
|
|
1194
|
+
Returns
|
|
1195
|
+
-------
|
|
1196
|
+
x_best : float
|
|
1197
|
+
z_best : float
|
|
1198
|
+
n_iter : int
|
|
1199
|
+
x_trace : list of float
|
|
1200
|
+
|
|
1201
|
+
Port of MATLAB ``infglob``.
|
|
1202
|
+
"""
|
|
1203
|
+
defaults = dict(
|
|
1204
|
+
display='off',
|
|
1205
|
+
tol=1e-4,
|
|
1206
|
+
maxIter=500,
|
|
1207
|
+
dispIter=10,
|
|
1208
|
+
dim=1,
|
|
1209
|
+
r=2.0,
|
|
1210
|
+
ksi=1.0,
|
|
1211
|
+
guess=[],
|
|
1212
|
+
)
|
|
1213
|
+
if options:
|
|
1214
|
+
defaults = updateopt(defaults, options)
|
|
1215
|
+
opt = defaults
|
|
1216
|
+
|
|
1217
|
+
verbosity = {'off': 0, 'final': 1, 'on': 2}.get(opt['display'], 0)
|
|
1218
|
+
tol = float(opt['tol'])
|
|
1219
|
+
max_iter = int(opt['maxIter'])
|
|
1220
|
+
disp_iter = int(opt['dispIter'])
|
|
1221
|
+
n = int(opt['dim'])
|
|
1222
|
+
r = float(opt['r'])
|
|
1223
|
+
ksi = float(opt['ksi'])
|
|
1224
|
+
guess = list(opt.get('guess', []))
|
|
1225
|
+
|
|
1226
|
+
x = sorted(set(list(np.arange(0, 1.1, 0.1)) + [float(g) for g in guess]))
|
|
1227
|
+
x = np.array(x, dtype=float)
|
|
1228
|
+
x = np.clip(x, 0.0, 1.0)
|
|
1229
|
+
z = np.array([func(xi) for xi in x], dtype=float)
|
|
1230
|
+
x_trace = list(x)
|
|
1231
|
+
k = len(x)
|
|
1232
|
+
|
|
1233
|
+
best_i = int(np.argmin(z))
|
|
1234
|
+
z_best = float(z[best_i])
|
|
1235
|
+
x_best = float(x[best_i])
|
|
1236
|
+
|
|
1237
|
+
lam = np.zeros(k + max_iter)
|
|
1238
|
+
|
|
1239
|
+
iteration = 0
|
|
1240
|
+
while True:
|
|
1241
|
+
iteration += 1
|
|
1242
|
+
if iteration > max_iter:
|
|
1243
|
+
break
|
|
1244
|
+
|
|
1245
|
+
# Sort
|
|
1246
|
+
order = np.argsort(x)
|
|
1247
|
+
x = x[order]; z = z[order]
|
|
1248
|
+
|
|
1249
|
+
dxn = (x[1:] - x[:-1]) ** (1.0 / n)
|
|
1250
|
+
dz = z[1:] - z[:-1]
|
|
1251
|
+
adz = np.abs(dz)
|
|
1252
|
+
with np.errstate(divide='ignore', invalid='ignore'):
|
|
1253
|
+
zDx = np.where(dxn > 1e-300, adz / dxn, 0.0)
|
|
1254
|
+
|
|
1255
|
+
X = np.max(dxn)
|
|
1256
|
+
mu = np.max(zDx) if len(zDx) > 0 else 1.0
|
|
1257
|
+
gamma = mu * dxn / max(X, 1e-300)
|
|
1258
|
+
|
|
1259
|
+
zDx_ext = np.concatenate([[zDx[0]], zDx, [zDx[-1]]])
|
|
1260
|
+
for i in range(k - 1):
|
|
1261
|
+
lam[i] = np.max(zDx_ext[i: i + 3])
|
|
1262
|
+
|
|
1263
|
+
hat_L = np.maximum(np.maximum(gamma, lam[:k - 1]), ksi)
|
|
1264
|
+
rLx = r * hat_L * dxn
|
|
1265
|
+
|
|
1266
|
+
with np.errstate(divide='ignore', invalid='ignore'):
|
|
1267
|
+
R = (rLx
|
|
1268
|
+
- 2 * (z[1:] + z[:-1])
|
|
1269
|
+
+ np.where(rLx > 1e-300, dz ** 2 / rLx, 0.0))
|
|
1270
|
+
|
|
1271
|
+
t = int(np.argmax(R))
|
|
1272
|
+
|
|
1273
|
+
with np.errstate(divide='ignore', invalid='ignore'):
|
|
1274
|
+
x_new = (x[t + 1] + x[t]) / 2.0 - (
|
|
1275
|
+
np.sign(dz[t]) * (adz[t] / max(hat_L[t], 1e-300)) ** n / (2.0 * r)
|
|
1276
|
+
)
|
|
1277
|
+
x_new = float(np.clip(x_new, 0.0, 1.0))
|
|
1278
|
+
z_new = float(func(x_new))
|
|
1279
|
+
|
|
1280
|
+
if z_new < z_best:
|
|
1281
|
+
z_best = z_new
|
|
1282
|
+
x_best = x_new
|
|
1283
|
+
|
|
1284
|
+
if verbosity > 1 and iteration % disp_iter == 0:
|
|
1285
|
+
print(f" infglob iter {iteration}: x={x_best:.5f} f={z_best:.5g}")
|
|
1286
|
+
|
|
1287
|
+
if abs(x[t + 1] - x[t]) < tol:
|
|
1288
|
+
break
|
|
1289
|
+
|
|
1290
|
+
x = np.append(x, x_new)
|
|
1291
|
+
x_trace.append(x_new)
|
|
1292
|
+
z = np.append(z, z_new)
|
|
1293
|
+
k += 1
|
|
1294
|
+
|
|
1295
|
+
if verbosity > 0:
|
|
1296
|
+
status = "not converged" if iteration > max_iter else "converged"
|
|
1297
|
+
print(f"infglob {status}: x={x_best:.5f}, f={z_best:.5g}, iter={iteration}")
|
|
1298
|
+
|
|
1299
|
+
return x_best, z_best, iteration, x_trace
|
|
1300
|
+
|
|
1301
|
+
|
|
1302
|
+
# ---------------------------------------------------------------------------
|
|
1303
|
+
# infglobc – constrained information algorithm
|
|
1304
|
+
# ---------------------------------------------------------------------------
|
|
1305
|
+
|
|
1306
|
+
def infglobc(func, options=None):
|
|
1307
|
+
"""
|
|
1308
|
+
Constrained global optimization using Strongin's information algorithm.
|
|
1309
|
+
|
|
1310
|
+
The function must return a list/array:
|
|
1311
|
+
- length < nConstr+1: some constraint is violated (returns partial values)
|
|
1312
|
+
- length == nConstr+1: all constraints satisfied; last element is objective
|
|
1313
|
+
|
|
1314
|
+
Parameters
|
|
1315
|
+
----------
|
|
1316
|
+
func : callable — f(x) -> list of length 1..nConstr+1
|
|
1317
|
+
options : dict, optional
|
|
1318
|
+
dim, nConstr, tol, r, maxIter, dispIter, display
|
|
1319
|
+
|
|
1320
|
+
Returns
|
|
1321
|
+
-------
|
|
1322
|
+
x_best, z_best, n_iter, x_trace
|
|
1323
|
+
|
|
1324
|
+
Port of MATLAB ``infglobc``.
|
|
1325
|
+
"""
|
|
1326
|
+
defaults = dict(
|
|
1327
|
+
display='off',
|
|
1328
|
+
tol=1e-4,
|
|
1329
|
+
maxIter=500,
|
|
1330
|
+
dispIter=50,
|
|
1331
|
+
dim=1,
|
|
1332
|
+
r=2.0,
|
|
1333
|
+
nConstr=1,
|
|
1334
|
+
)
|
|
1335
|
+
if options:
|
|
1336
|
+
defaults = updateopt(defaults, options)
|
|
1337
|
+
opt = defaults
|
|
1338
|
+
|
|
1339
|
+
verbosity = {'off': 0, 'final': 1, 'on': 2}.get(opt['display'], 0)
|
|
1340
|
+
tol = float(opt['tol'])
|
|
1341
|
+
max_iter = int(opt['maxIter'])
|
|
1342
|
+
disp_iter = int(opt['dispIter'])
|
|
1343
|
+
n = int(opt['dim'])
|
|
1344
|
+
r = float(opt['r'])
|
|
1345
|
+
n_constr = int(opt['nConstr'])
|
|
1346
|
+
|
|
1347
|
+
eps_r = 0.1 * np.ones(n_constr)
|
|
1348
|
+
|
|
1349
|
+
def _eval(xi):
|
|
1350
|
+
g = func(float(xi))
|
|
1351
|
+
if isinstance(g, (int, float)):
|
|
1352
|
+
g = [g]
|
|
1353
|
+
return list(g)
|
|
1354
|
+
|
|
1355
|
+
x = [0.0, 1.0]
|
|
1356
|
+
g0 = _eval(0.0)
|
|
1357
|
+
g1 = _eval(1.0)
|
|
1358
|
+
|
|
1359
|
+
nu = [len(g0), len(g1)]
|
|
1360
|
+
g_arr = [g0, g1]
|
|
1361
|
+
z = [g0[-1], g1[-1]]
|
|
1362
|
+
x_trace = list(x)
|
|
1363
|
+
|
|
1364
|
+
ind_feas = [i for i, ni in enumerate(nu) if ni == n_constr + 1]
|
|
1365
|
+
if ind_feas:
|
|
1366
|
+
z_best = min(z[i] for i in ind_feas)
|
|
1367
|
+
x_best = x[ind_feas[np.argmin([z[i] for i in ind_feas])]]
|
|
1368
|
+
else:
|
|
1369
|
+
z_best = np.inf
|
|
1370
|
+
x_best = None
|
|
1371
|
+
|
|
1372
|
+
mu = np.ones(n_constr + 1)
|
|
1373
|
+
k = 2
|
|
1374
|
+
|
|
1375
|
+
for iteration in range(1, max_iter + 1):
|
|
1376
|
+
# Sort by x
|
|
1377
|
+
order = np.argsort(x)
|
|
1378
|
+
x = [x[i] for i in order]
|
|
1379
|
+
z = [z[i] for i in order]
|
|
1380
|
+
nu = [nu[i] for i in order]
|
|
1381
|
+
g_arr = [g_arr[i] for i in order]
|
|
1382
|
+
|
|
1383
|
+
x_arr = np.array(x)
|
|
1384
|
+
z_arr = np.array(z)
|
|
1385
|
+
nu_arr = np.array(nu)
|
|
1386
|
+
|
|
1387
|
+
# Compute mu for each constraint level
|
|
1388
|
+
z_ast = np.zeros(n_constr + 1)
|
|
1389
|
+
Iv = list(range(k))
|
|
1390
|
+
for v in range(1, n_constr + 2):
|
|
1391
|
+
if len(Iv) < 2:
|
|
1392
|
+
mu[v - 1] = 1.0
|
|
1393
|
+
else:
|
|
1394
|
+
mu_v = 0.0
|
|
1395
|
+
for ii in range(len(Iv) - 1):
|
|
1396
|
+
for jj in range(ii + 1, len(Iv)):
|
|
1397
|
+
xi_ii, xi_jj = x_arr[Iv[ii]], x_arr[Iv[jj]]
|
|
1398
|
+
gi_ii = g_arr[Iv[ii]][v - 1] if len(g_arr[Iv[ii]]) >= v else np.inf
|
|
1399
|
+
gi_jj = g_arr[Iv[jj]][v - 1] if len(g_arr[Iv[jj]]) >= v else np.inf
|
|
1400
|
+
dx_ = abs(xi_jj - xi_ii)
|
|
1401
|
+
if dx_ > 1e-300:
|
|
1402
|
+
mu_v = max(mu_v,
|
|
1403
|
+
abs(gi_jj - gi_ii) / dx_ ** (1.0 / n))
|
|
1404
|
+
mu[v - 1] = max(mu_v, 1.0)
|
|
1405
|
+
|
|
1406
|
+
Iv_next = [i for i in Iv if nu_arr[i] >= v + 1]
|
|
1407
|
+
if not Iv_next:
|
|
1408
|
+
z_ast[v - 1] = min(z_arr[i] for i in Iv)
|
|
1409
|
+
break
|
|
1410
|
+
else:
|
|
1411
|
+
z_ast[v - 1] = -eps_r[v - 1] if v - 1 < len(eps_r) else 0.0
|
|
1412
|
+
Iv = Iv_next
|
|
1413
|
+
|
|
1414
|
+
# Compute R for each interval
|
|
1415
|
+
Delta_arr = (x_arr[1:] - x_arr[:-1]) ** (1.0 / n)
|
|
1416
|
+
R = np.zeros(k - 1)
|
|
1417
|
+
for i in range(k - 1):
|
|
1418
|
+
v = max(nu_arr[i], nu_arr[i + 1])
|
|
1419
|
+
rMu = r * mu[v - 1]
|
|
1420
|
+
rMuDelta = rMu * Delta_arr[i]
|
|
1421
|
+
dz_i = z_arr[i + 1] - z_arr[i]
|
|
1422
|
+
if nu_arr[i] == nu_arr[i + 1]:
|
|
1423
|
+
R[i] = (rMuDelta
|
|
1424
|
+
+ dz_i ** 2 / max(rMuDelta, 1e-300)
|
|
1425
|
+
- 2 * (z_arr[i + 1] + z_arr[i] - 2 * z_ast[v - 1]))
|
|
1426
|
+
elif nu_arr[i] < nu_arr[i + 1]:
|
|
1427
|
+
R[i] = (2 * Delta_arr[i]
|
|
1428
|
+
- 4 * (z_arr[i + 1] - z_ast[v - 1]) / max(rMu, 1e-300))
|
|
1429
|
+
else:
|
|
1430
|
+
R[i] = (2 * Delta_arr[i]
|
|
1431
|
+
- 4 * (z_arr[i] - z_ast[v - 1]) / max(rMu, 1e-300))
|
|
1432
|
+
|
|
1433
|
+
t = int(np.argmax(R))
|
|
1434
|
+
v_t = max(nu_arr[t], nu_arr[t + 1])
|
|
1435
|
+
dz_t = z_arr[t + 1] - z_arr[t]
|
|
1436
|
+
x_new = (x_arr[t + 1] + x_arr[t]) / 2.0
|
|
1437
|
+
if nu_arr[t] == nu_arr[t + 1]:
|
|
1438
|
+
x_new -= (np.sign(dz_t) * abs(dz_t) / max(mu[v_t - 1], 1e-300)
|
|
1439
|
+
) ** n / (2.0 * r)
|
|
1440
|
+
x_new = float(np.clip(x_new, 0.0, 1.0))
|
|
1441
|
+
|
|
1442
|
+
g_new = _eval(x_new)
|
|
1443
|
+
nu_new = len(g_new)
|
|
1444
|
+
|
|
1445
|
+
if nu_new == n_constr + 1:
|
|
1446
|
+
z_new = g_new[-1]
|
|
1447
|
+
if z_new < z_best:
|
|
1448
|
+
z_best = z_new
|
|
1449
|
+
x_best = x_new
|
|
1450
|
+
|
|
1451
|
+
if verbosity > 1 and iteration % disp_iter == 0:
|
|
1452
|
+
if x_best is not None:
|
|
1453
|
+
print(f" infglobc iter {iteration}: x={x_best:.6f} f={z_best:.5g}")
|
|
1454
|
+
|
|
1455
|
+
if abs(x_arr[t + 1] - x_arr[t]) < tol:
|
|
1456
|
+
break
|
|
1457
|
+
|
|
1458
|
+
x.append(x_new); x_trace.append(x_new)
|
|
1459
|
+
z.append(g_new[-1]); nu.append(nu_new)
|
|
1460
|
+
g_arr.append(g_new)
|
|
1461
|
+
k += 1
|
|
1462
|
+
|
|
1463
|
+
if verbosity > 0:
|
|
1464
|
+
print(f"infglobc done: f={z_best:.5g}, iter={iteration}")
|
|
1465
|
+
|
|
1466
|
+
return x_best, z_best, iteration, x_trace
|
|
1467
|
+
|
|
1468
|
+
|
|
1469
|
+
# ---------------------------------------------------------------------------
|
|
1470
|
+
# optglob – multi-run infglob with zooming
|
|
1471
|
+
# ---------------------------------------------------------------------------
|
|
1472
|
+
|
|
1473
|
+
def optglob(func, options=None, limits=None):
|
|
1474
|
+
"""
|
|
1475
|
+
Global optimization via repeated infglob with limit zooming.
|
|
1476
|
+
|
|
1477
|
+
Parameters
|
|
1478
|
+
----------
|
|
1479
|
+
func : callable — f(x) -> float, x ∈ [0, 1]
|
|
1480
|
+
options : dict, optional
|
|
1481
|
+
Fields: tol, maxIter, maxLoop, display, bounds, decLim, incR, r
|
|
1482
|
+
limits : np.ndarray (Ncoef × 2), optional — current search bounds
|
|
1483
|
+
(passed via context; if None, uses [[0,1]])
|
|
1484
|
+
|
|
1485
|
+
Returns
|
|
1486
|
+
-------
|
|
1487
|
+
x_best, z_best, coef, n_iter, x_trace
|
|
1488
|
+
|
|
1489
|
+
Port of MATLAB ``optglob``.
|
|
1490
|
+
"""
|
|
1491
|
+
defaults = dict(
|
|
1492
|
+
display='off',
|
|
1493
|
+
tol=1e-4,
|
|
1494
|
+
tolLim=0.01,
|
|
1495
|
+
maxIter=100,
|
|
1496
|
+
maxLoop=20,
|
|
1497
|
+
bounds=False,
|
|
1498
|
+
decLim=2.5,
|
|
1499
|
+
incR=1.2,
|
|
1500
|
+
r=2.0,
|
|
1501
|
+
)
|
|
1502
|
+
if options:
|
|
1503
|
+
defaults = updateopt(defaults, options)
|
|
1504
|
+
opt = defaults
|
|
1505
|
+
|
|
1506
|
+
verbosity = {'off': 0, 'final': 1, 'on': 2}.get(opt['display'], 0)
|
|
1507
|
+
max_loop = int(opt['maxLoop'])
|
|
1508
|
+
dec_lim = float(opt['decLim'])
|
|
1509
|
+
inc_r = float(opt['incR'])
|
|
1510
|
+
check_bounds = bool(opt['bounds'])
|
|
1511
|
+
|
|
1512
|
+
if limits is None:
|
|
1513
|
+
Lim = np.array([[0.0, 1.0]])
|
|
1514
|
+
else:
|
|
1515
|
+
Lim = np.asarray(limits, dtype=float).reshape(-1, 2)
|
|
1516
|
+
Lim0 = Lim.copy()
|
|
1517
|
+
N_coef = Lim.shape[0]
|
|
1518
|
+
w = np.zeros(N_coef)
|
|
1519
|
+
|
|
1520
|
+
coef_best = None
|
|
1521
|
+
z_best = np.inf
|
|
1522
|
+
x_best = None
|
|
1523
|
+
x_trace = []
|
|
1524
|
+
|
|
1525
|
+
ig_opt = dict(opt)
|
|
1526
|
+
ig_opt['maxIter'] = int(opt['maxIter'])
|
|
1527
|
+
|
|
1528
|
+
for loop in range(max_loop):
|
|
1529
|
+
if verbosity > 1:
|
|
1530
|
+
print(f" optglob run {loop + 1}")
|
|
1531
|
+
|
|
1532
|
+
xb, zb, _, xt = infglob(func, ig_opt)
|
|
1533
|
+
x_trace += xt
|
|
1534
|
+
|
|
1535
|
+
if xb is not None and zb < z_best:
|
|
1536
|
+
z_best = zb
|
|
1537
|
+
x_best = xb
|
|
1538
|
+
coef_best = hilb2coord(xb, N_coef, int(ig_opt.get('bits', 8)))
|
|
1539
|
+
|
|
1540
|
+
if loop + 1 >= max_loop:
|
|
1541
|
+
break
|
|
1542
|
+
|
|
1543
|
+
# Zoom limits
|
|
1544
|
+
if coef_best is not None:
|
|
1545
|
+
if np.max(np.abs(Lim[:, 1] - Lim[:, 0])) < opt['tolLim']:
|
|
1546
|
+
break
|
|
1547
|
+
for i in range(N_coef):
|
|
1548
|
+
w[i] = (Lim[i, 1] - Lim[i, 0]) / dec_lim
|
|
1549
|
+
Lim[i, 0] = coef_best[i] - w[i]
|
|
1550
|
+
Lim[i, 1] = coef_best[i] + w[i]
|
|
1551
|
+
else:
|
|
1552
|
+
for i in range(N_coef):
|
|
1553
|
+
ci = (Lim[i, 1] + Lim[i, 0]) / 2.0
|
|
1554
|
+
w[i] = (Lim[i, 1] - Lim[i, 0]) / dec_lim
|
|
1555
|
+
Lim[i, 0] = ci - w[i]
|
|
1556
|
+
Lim[i, 1] = ci + w[i]
|
|
1557
|
+
|
|
1558
|
+
if check_bounds:
|
|
1559
|
+
for i in range(N_coef):
|
|
1560
|
+
if Lim[i, 0] < Lim0[i, 0]:
|
|
1561
|
+
Lim[i, 0] = Lim0[i, 0]; Lim[i, 1] = Lim0[i, 0] + 2 * w[i]
|
|
1562
|
+
if Lim[i, 1] > Lim0[i, 1]:
|
|
1563
|
+
Lim[i, 1] = Lim0[i, 1]; Lim[i, 0] = Lim0[i, 1] - 2 * w[i]
|
|
1564
|
+
|
|
1565
|
+
ig_opt['r'] = min(10.0, inc_r * ig_opt.get('r', 2.0))
|
|
1566
|
+
|
|
1567
|
+
return x_best, z_best, coef_best, loop + 1, x_trace
|
|
1568
|
+
|
|
1569
|
+
|
|
1570
|
+
# ---------------------------------------------------------------------------
|
|
1571
|
+
# optglobc – constrained multi-run infglobc with zooming
|
|
1572
|
+
# ---------------------------------------------------------------------------
|
|
1573
|
+
|
|
1574
|
+
def optglobc(func, options=None, limits=None):
|
|
1575
|
+
"""
|
|
1576
|
+
Constrained global optimization via repeated infglobc with zooming.
|
|
1577
|
+
|
|
1578
|
+
Parameters and returns as optglob, but func follows the infglobc
|
|
1579
|
+
convention (returns partial vector when constraints are violated).
|
|
1580
|
+
|
|
1581
|
+
Port of MATLAB ``optglobc``.
|
|
1582
|
+
"""
|
|
1583
|
+
defaults = dict(
|
|
1584
|
+
display='off',
|
|
1585
|
+
tol=1e-4,
|
|
1586
|
+
tolLim=0.01,
|
|
1587
|
+
maxIter=100,
|
|
1588
|
+
maxLoop=20,
|
|
1589
|
+
nConstr=1,
|
|
1590
|
+
bounds=False,
|
|
1591
|
+
decLim=2.0,
|
|
1592
|
+
incR=1.2,
|
|
1593
|
+
r=2.0,
|
|
1594
|
+
)
|
|
1595
|
+
if options:
|
|
1596
|
+
defaults = updateopt(defaults, options)
|
|
1597
|
+
opt = defaults
|
|
1598
|
+
|
|
1599
|
+
verbosity = {'off': 0, 'final': 1, 'on': 2}.get(opt['display'], 0)
|
|
1600
|
+
max_loop = int(opt['maxLoop'])
|
|
1601
|
+
dec_lim = float(opt['decLim'])
|
|
1602
|
+
inc_r = float(opt['incR'])
|
|
1603
|
+
check_bounds = bool(opt['bounds'])
|
|
1604
|
+
|
|
1605
|
+
if limits is None:
|
|
1606
|
+
Lim = np.array([[0.0, 1.0]])
|
|
1607
|
+
else:
|
|
1608
|
+
Lim = np.asarray(limits, dtype=float).reshape(-1, 2)
|
|
1609
|
+
Lim0 = Lim.copy()
|
|
1610
|
+
N_coef = Lim.shape[0]
|
|
1611
|
+
w = np.zeros(N_coef)
|
|
1612
|
+
|
|
1613
|
+
coef_best = None
|
|
1614
|
+
z_best = np.inf
|
|
1615
|
+
x_best = None
|
|
1616
|
+
x_trace = []
|
|
1617
|
+
|
|
1618
|
+
ig_opt = dict(opt)
|
|
1619
|
+
|
|
1620
|
+
for loop in range(max_loop):
|
|
1621
|
+
if verbosity > 1:
|
|
1622
|
+
print(f" optglobc run {loop + 1}")
|
|
1623
|
+
|
|
1624
|
+
xb, zb, _, xt = infglobc(func, ig_opt)
|
|
1625
|
+
x_trace += xt
|
|
1626
|
+
|
|
1627
|
+
if xb is not None and zb < z_best:
|
|
1628
|
+
z_best = zb
|
|
1629
|
+
x_best = xb
|
|
1630
|
+
coef_best = hilb2coord(xb, N_coef, int(ig_opt.get('bits', 8)))
|
|
1631
|
+
|
|
1632
|
+
if loop + 1 >= max_loop:
|
|
1633
|
+
break
|
|
1634
|
+
|
|
1635
|
+
if coef_best is not None:
|
|
1636
|
+
if np.max(np.abs(Lim[:, 1] - Lim[:, 0])) < opt['tolLim']:
|
|
1637
|
+
break
|
|
1638
|
+
for i in range(N_coef):
|
|
1639
|
+
w[i] = (Lim[i, 1] - Lim[i, 0]) / dec_lim
|
|
1640
|
+
Lim[i, 0] = coef_best[i] - w[i]
|
|
1641
|
+
Lim[i, 1] = coef_best[i] + w[i]
|
|
1642
|
+
else:
|
|
1643
|
+
for i in range(N_coef):
|
|
1644
|
+
ci = (Lim[i, 1] + Lim[i, 0]) / 2.0
|
|
1645
|
+
w[i] = (Lim[i, 1] - Lim[i, 0]) / dec_lim
|
|
1646
|
+
Lim[i, 0] = ci - w[i]
|
|
1647
|
+
Lim[i, 1] = ci + w[i]
|
|
1648
|
+
|
|
1649
|
+
if check_bounds:
|
|
1650
|
+
for i in range(N_coef):
|
|
1651
|
+
if Lim[i, 0] < Lim0[i, 0]:
|
|
1652
|
+
Lim[i, 0] = Lim0[i, 0]; Lim[i, 1] = Lim0[i, 0] + 2 * w[i]
|
|
1653
|
+
if Lim[i, 1] > Lim0[i, 1]:
|
|
1654
|
+
Lim[i, 1] = Lim0[i, 1]; Lim[i, 0] = Lim0[i, 1] - 2 * w[i]
|
|
1655
|
+
|
|
1656
|
+
ig_opt['r'] = min(10.0, inc_r * ig_opt.get('r', 2.0))
|
|
1657
|
+
|
|
1658
|
+
return x_best, z_best, coef_best, loop + 1, x_trace
|