@@ -166,78 +166,62 @@ def _set_coordinates(self, coord, score=None):
166
166
score = self ._score_func (coord )
167
167
self ._scores .append (score )
168
168
169
- def optimize (self , n_steps ,
170
- beta_start , rate_beta , stepsize_start , stepsize_end ):
169
+ def optimize (self , n_steps = 20000 ,
170
+ beta_start = 1 , beta_end = 500 ,
171
+ stepsize_start = 10 , stepsize_end = 0.2 ):
171
172
r"""
172
- Perform a Simulated Annealing optimization on the current
173
+ Perform a simulated annealing optimization on the current
173
174
coordinate to minimize the score returned by the score function.
174
175
175
- This is basically a Monte-Carlo optimization where the
176
- temperature is varied according to a so called annealing
177
- schedule over the course of the optimization.
176
+ This is basically a Metropolis-Monte-Carlo optimization where
177
+ the inverse temperature and step size is varied according to
178
+ exponential annealing schedule over the course of the
179
+ optimization.
180
+
181
+ Parameters
182
+ ----------
183
+ n_steps : int
184
+ The number of simulated annealing steps.
185
+ beta_start, beta_end : float
186
+ The inverse temperature in the first and last step of the
187
+ optimization, respectively.
188
+ Higher values allow less increase of score, i.e. result
189
+ in a steering into the local minimum.
190
+ Must be positive.
191
+ stepsize_start, stepsize_end : float
192
+ The step size in the first and last step of the
193
+ optimization, respectively.
194
+ it is the radius in which the coordinates are randomly
195
+ altered at in each optimization step.
196
+ Must be positive.
197
+
198
+ Notes
199
+ -----
178
200
The algorithm is a heuristic thats motivated by the physical
179
201
process of annealing.
180
202
If we, e.g., cool steel than a slow cooling can yield a superior
181
203
quality, whereas for a fast cooling the steel can become
182
204
brittle.
183
205
The same happens here within the search space for the given
184
- minimization task.
185
-
186
- Parameters
187
- ----------
188
- n_steps : int
189
- The number of Simulated-Annealing steps.
190
- beta_start : float
191
- The inverse start temperature, where the start temperature
192
- would be :math:`T_{start} = 1/(k_b \cdot \beta_{start})` with
193
- :math:`k_b` being the boltzmann constant.
194
- rate_beta: float
195
- The rate controls how fast the inverse temperature is
196
- increased within the annealing schedule.
197
- Here the exponential schedule is chosen so we have
198
- :math:`\beta (t) = \beta_0 \cdot \exp(rate \cdot t)`.
199
- stepsize_start : float
200
- The radius in which the coordinates are randomly altered at
201
- the beginning of the simulated anneling algorithm.
202
- Like the inverse temperature the step size follows an
203
- exponential schedule, enabling the algorithm
204
- to do large perturbartions at the beginning of the algorithm
205
- run and increasingly smaller ones afterwards.
206
- stepsize_end : float
207
- The radius in which the coordinates are randomly altered at
208
- the end of the simulated annealing algorithm run.
206
+ minimization task.
209
207
"""
210
- # Calculate the max value 'i' can reach so that
211
- # 'np.exp(rate_beta*i)' does not overflow
212
- max_i = np .log (np .finfo (np .float64 ).max ) / rate_beta
213
- beta = lambda i : beta_start * np .exp (rate_beta * i ) \
214
- if i < max_i else np .inf
215
-
216
- # Choose rate so that stepsize_end reached after n_steps
217
- # derived from step_size(N_steps) = steps_end
218
- if stepsize_start == stepsize_end :
219
- rate_stepsize = 0
220
- else :
221
- rate_stepsize = np .log (stepsize_end / stepsize_start ) / n_steps
222
- step_size = lambda i : stepsize_start * np .exp (rate_stepsize * i )
208
+ betas = _calculate_schedule (n_steps , beta_start , beta_end )
209
+ stepsizes = _calculate_schedule (n_steps , stepsize_start , stepsize_end )
223
210
224
211
for i in range (n_steps ):
225
-
226
212
score = self ._scores [- 1 ]
227
213
new_coord = self ._sample_coord (
228
214
self ._coord ,
229
- lambda c : c + (random .rand (* c .shape )- 0.5 ) * 2 * step_size ( i )
215
+ lambda c : c + (random .rand (* c .shape )- 0.5 ) * 2 * stepsizes [ i ]
230
216
)
231
217
new_score = self ._score_func (new_coord )
232
218
233
219
if new_score < score :
234
220
self ._set_coordinates (new_coord , new_score )
235
221
236
222
else :
237
- p_accept = np .exp ( - beta (i ) * (new_score - score ))
238
- p = random .rand ()
239
-
240
- if p <= p_accept :
223
+ p_accept = np .exp ( - betas [i ] * (new_score - score ))
224
+ if random .rand () <= p_accept :
241
225
self ._set_coordinates (new_coord , new_score )
242
226
else :
243
227
self ._set_coordinates (self ._coord , score )
@@ -292,6 +276,15 @@ def _apply_constraints(self, coord):
292
276
coord [self ._constraint_mask ] = self ._constraints [self ._constraint_mask ]
293
277
294
278
279
+ def _calculate_schedule (n_steps , start , end ):
280
+ """
281
+ Calculate the values for each step in an exponential schedule.
282
+ """
283
+ # Use float 64
284
+ return start * (end / start )** np .linspace (0 , 1 , n_steps , dtype = np .float64 )
285
+
286
+
287
+
295
288
class ScoreFunction (metaclass = abc .ABCMeta ):
296
289
"""
297
290
Abstract base class for a score function.
@@ -396,19 +389,18 @@ def __call__(self, coord):
396
389
@staticmethod
397
390
def _calculate_distances (tri_indices , coord , distance_formula ):
398
391
ind1 , ind2 = tri_indices
399
- if distance_formula == "CIEDE76 " :
400
- dist = skimage . color . deltaE_ciede94 (
401
- coord [ind1 ], coord [ind2 ]
392
+ if distance_formula == "CIE76 " :
393
+ return np . sqrt (
394
+ np . sum (( coord [ind1 , :] - coord [ind2 , :]) ** 2 , axis = - 1 )
402
395
)
403
396
elif distance_formula == "CIEDE94" :
404
- dist = skimage .color .deltaE_cie76 (
397
+ return skimage .color .deltaE_ciede94 (
405
398
coord [ind1 ], coord [ind2 ]
406
399
)
407
400
else : #"CIEDE2000"
408
- dist = skimage .color .deltaE_ciede2000 (
401
+ return skimage .color .deltaE_ciede2000 (
409
402
coord [ind1 ], coord [ind2 ]
410
403
)
411
- return dist
412
404
413
405
@staticmethod
414
406
def _calculate_ideal_distances (tri_indices , substitution_matrix ):
@@ -419,4 +411,4 @@ def _calculate_ideal_distances(tri_indices, substitution_matrix):
419
411
distances = dist_matrix [ind_i , ind_j ]
420
412
# Scale, so that average distance is 1
421
413
distances /= np .average (distances )
422
- return distances
414
+ return distances
0 commit comments