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