Skip to content

Commit 09b5120

Browse files
authored
feature: Improved Progress Bar to iteration based + showing convergence as % (#10)
feature: Improved Progress Bar: iteration based + showing convergence as %
2 parents c7e5b26 + 72dff12 commit 09b5120

File tree

4 files changed

+388
-1596
lines changed

4 files changed

+388
-1596
lines changed

.github/workflows/nightly_tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
- name: pip install
2929
run: |
3030
pip install wakis['notebook']
31-
pip install iddefix
31+
pip install iddefix pymoo
3232
3333
- name: Print versions
3434
run: conda list

examples/005_cmaes_examples.ipynb

Lines changed: 283 additions & 1556 deletions
Large diffs are not rendered by default.

iddefix/smartBoundDetermination.py

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ class SmartBoundDetermination:
1515

1616
def __init__(self, frequency_data, impedance_data,
1717
minimum_peak_height=1.0,
18+
threshold=None,
19+
distance=None,
20+
prominence=None,
1821
Rs_bounds=[0.8, 10],
1922
Q_bounds=[0.5, 5],
2023
fres_bounds=[-0.01e9, +0.01e9]):
@@ -34,6 +37,16 @@ def __init__(self, frequency_data, impedance_data,
3437
Impedance magnitude data in Ohms.
3538
minimum_peak_height : float, optional
3639
Minimum height for a peak to be considered a resonance. Default is 1.0.
40+
threshold : float, optional
41+
Required vertical distance between a peak and its neighboring values
42+
to be considered a peak. Passed to `scipy.signal.find_peaks`. Default is None.
43+
distance : float, optional
44+
Required minimum horizontal distance (in indices) between peaks.
45+
Passed to `scipy.signal.find_peaks`. Default is None.
46+
prominence : float, optional
47+
Required prominence of peaks. The prominence measures how much a peak
48+
stands out compared to its surrounding values. Passed to `scipy.signal.find_peaks`.
49+
Default is None.
3750
Rs_bounds : list, optional
3851
Scaling factors [min, max] for Rs bounds. Default is [0.8, 10].
3952
Q_bounds : list, optional
@@ -78,11 +91,34 @@ def __init__(self, frequency_data, impedance_data,
7891
- Computed parameter bounds are stored in `self.parameterBounds`.
7992
- The `inspect()` method visualizes peak detection results.
8093
- The `to_table()` method prints a structured table of parameter ranges.
94+
95+
Returns
96+
-------
97+
parameterBounds : list of tuples
98+
A list of parameter bounds for fitting. Each resonance contributes
99+
three sets of bounds:
100+
- `(Rs_min, Rs_max)`: Bounds for resistance Rs.
101+
- `(Q_min, Q_max)`: Bounds for quality factor Q.
102+
- `(freq_min, freq_max)`: Bounds for the resonant frequency.
103+
104+
Notes
105+
-----
106+
- The peak-finding algorithm is implemented using `scipy.signal.find_peaks`.
107+
See the official documentation for more details:
108+
https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.find_peaks.html
109+
- The 3dB bandwidth method is used to estimate initial Q factors and
110+
define frequency bounds.
111+
- The detected peaks and their heights are stored in instance attributes
112+
`self.peaks` and `self.peaks_height`, respectively.
113+
- The number of detected resonances is stored in `self.Nres`.
81114
"""
82115

83116
self.frequency_data = frequency_data
84117
self.impedance_data = impedance_data
85118
self.minimum_peak_height = minimum_peak_height
119+
self.threshold = threshold
120+
self.distance = distance
121+
self.prominence = prominence
86122
self.Rs_bounds = Rs_bounds
87123
self.Q_bounds = Q_bounds
88124
self.fres_bounds = fres_bounds
@@ -159,7 +195,12 @@ def find(self, frequency_data=None, impedance_data=None,
159195
impedance_data = self.impedance_data
160196
if minimum_peak_height is None:
161197
minimum_peak_height = self.minimum_peak_height
162-
198+
if threshold is None:
199+
threshold = self.threshold
200+
if distance is None:
201+
distance = self.distance
202+
if prominence is None:
203+
prominence = self.prominence
163204

164205
# Find the peaks of the impedance data
165206
peaks, peaks_height = find_peaks(impedance_data,
@@ -197,6 +238,8 @@ def find(self, frequency_data=None, impedance_data=None,
197238
Q_bounds = (initial_Qs[i]*self.Q_bounds[0] , initial_Qs[i]*self.Q_bounds[1])
198239
freq_bounds = (frequency_data[peaks[i]]+self.fres_bounds[0], frequency_data[peaks[i]]+self.fres_bounds[1])
199240

241+
if peaks_height['peak_heights'][i] < 0:
242+
Rs_bounds = (Rs_bounds[1], Rs_bounds[0]) # Swap for negative peaks
200243
parameterBounds.extend([Rs_bounds, Q_bounds, freq_bounds])
201244

202245
# Store peaks and peaks_height as instance attributes
@@ -223,7 +266,7 @@ def inspect(self):
223266

224267
return None
225268

226-
def to_table(self, to_markdown=False):
269+
def to_table(self, parameterBounds=None, to_markdown=False):
227270
"""
228271
Displays resonance parameters in a formatted ASCII table.
229272
@@ -240,7 +283,7 @@ def to_table(self, to_markdown=False):
240283
2 | 85.61 to 864.12 | 120.55 to 200.23| 5.30e+08 to 7.23e+08
241284
------------------------------------------------------------
242285
"""
243-
params = self.parameterBounds
286+
params = self.parameterBounds if parameterBounds is None else parameterBounds
244287
N_resonators = len(params) // 3 # Compute number of resonators
245288

246289
# Define formatting

iddefix/solvers.py

Lines changed: 58 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,45 @@
1212
from scipy.optimize import differential_evolution
1313

1414
class ProgressBarCallback:
15-
def __init__(self, max_generations):
15+
def __init__(self, max_generations, desc="Optimization"):
1616
self.max_generations = max_generations
1717
self.current_generation = 0
18-
self.previous_convergence = 0
19-
self.pbar = tqdm(total=100, desc="Optimization Progress %")
20-
21-
def __call__(self, xk, convergence):
22-
self.current_generation += 1
23-
self.pbar.update((convergence-self.previous_convergence)*100) # Update the progress bar
24-
self.previous_convergence = convergence
25-
if convergence > 1.: # Convergence threshold
26-
self.pbar.close()
27-
return True # Stop optimization early
18+
self.pbar = tqdm(total=max_generations, desc=desc, unit="gen")
19+
20+
def __call__(self, *args):
21+
22+
# --- scipy differential_evolution: (xk, convergence) ---
23+
if len(args) == 2 and not hasattr(args[0], "evaluator"):
24+
xk, convergence = args
25+
self.current_generation += 1
26+
self.pbar.update(1)
27+
self.pbar.set_postfix({
28+
"conv": f"{(100*(convergence)):6.1f} %"
29+
})
30+
# optional early stopping
31+
if convergence >= 1.0:
32+
self.pbar.close()
33+
return True
34+
35+
# --- pymoo minimize: (algorithm) ---
36+
elif len(args) == 1:
37+
algorithm = args[0]
38+
self.current_generation += 1
39+
self.pbar.update(1)
40+
41+
# compute convergence metric
42+
F = np.atleast_1d(algorithm.pop.get("F"))
43+
gap = float(np.mean(F) - np.min(F))
44+
self._gap0 = getattr(self, "_gap0", gap if gap > 0 else 1.0)
45+
convergence = float(np.clip(1.0 - gap / (self._gap0 + 1e-12), 0.0, 1.0))
46+
47+
self.pbar.set_postfix({
48+
"conv": f"{(100*(convergence)):6.1f} %"
49+
})
50+
# optional early stopping
51+
if convergence >= 1.0 and self.current_generation > 1:
52+
self.pbar.close()
53+
return True
2854

2955
def close(self):
3056
self.pbar.close()
@@ -73,7 +99,7 @@ def run_scipy_solver(parameterBounds,
7399
- The solution found by the solver.
74100
- A message indicating the solver's status.
75101
"""
76-
pbar = ProgressBarCallback(maxiter)
102+
pbar = ProgressBarCallback(maxiter, desc='Differential Evolution')
77103
result = differential_evolution(minimization_function,
78104
parameterBounds,
79105
popsize=popsize,
@@ -91,22 +117,6 @@ def run_scipy_solver(parameterBounds,
91117
)
92118
pbar.close()
93119

94-
# Need to be reworked to use the last population as the new initial population to speed up convergence
95-
"""while ((result.message == 'Maximum number of iterations has been exceeded.') and (iteration_convergence)):
96-
warning = 'Increased number of iterations by 10% to reach convergence. \n'
97-
maxiter = int(1.1*maxiter)
98-
result = differential_evolution(minimization_function,parameterBounds,
99-
popsize=popsize, tol=tol, maxiter=maxiter,
100-
mutation=mutation, recombination=crossover_rate, polish=False,
101-
init='latinhypercube',
102-
callback=show_progress_bar,
103-
updating='deferred', workers=-1, #vectorized=vectorized
104-
)
105-
106-
else:
107-
warning = ''
108-
"""
109-
110120
solution, message = result.x, result.message
111121

112122
return solution, message
@@ -206,8 +216,9 @@ def run_pyfde_jade_solver(parameterBounds,
206216
def run_pymoo_cmaes_solver(parameterBounds,
207217
minimization_function,
208218
sigma=0.1,
209-
maxiter=1000,
210-
popsize=50,
219+
maxiter=None, # default: 100 + 150 * (N+3)**2 // popsize**0.5
220+
popsize=None, # defaul: 4 + int(3 * np.log(len(parameterBounds)))
221+
verbose=False,
211222
**kwargs):
212223
"""
213224
Runs the pymoo CMAES solver to minimize a given function.
@@ -229,10 +240,10 @@ def run_pymoo_cmaes_solver(parameterBounds,
229240
from pymoo.core.problem import Problem
230241
from pymoo.optimize import minimize
231242
from pymoo.termination import get_termination
232-
except:
233-
ImportError('''Please install the pymoo package to use the CMA-ES solver:
234-
>>> pip install pymoo
235-
''')
243+
except ImportError:
244+
raise ImportError('''Please install the pymoo package to use the CMA-ES solver:
245+
>>> pip install pymoo
246+
''')
236247

237248
class OptimizationProblem(Problem):
238249
def __init__(self, objective_function, n_var, n_obj, xl, xu):
@@ -262,14 +273,25 @@ def _evaluate(self, x, out):
262273
sigma=sigma,
263274
popsize=popsize,
264275
seed=42,
276+
restarts=3,
277+
restart_from_best=True,
265278
**kwargs,
266279
)
267280
# use ftol and n_gen as stopping criteria
268281
termination_criteria = get_termination("n_gen", maxiter)
269282

270-
res = minimize(problem, solver, termination_criteria, seed=42, verbose=True)
283+
if not verbose:
284+
cb = ProgressBarCallback(maxiter, desc="CMA-ES evolution")
285+
else: cb = None
286+
287+
res = minimize(problem, solver, termination_criteria,
288+
seed=42,
289+
callback=cb,
290+
verbose=verbose)
291+
292+
if not verbose: cb.close()
271293

272294
solution = res.X
273-
message = "Convergence achieved" #if res. < maxiter else "Maximum iterations reached"
295+
message = "Convergence achieved" if res.algorithm.n_gen < maxiter else "Maximum iterations reached"
274296

275297
return solution, message, res

0 commit comments

Comments
 (0)