SearchLibrium 0.0.1__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.
old_code/harmony.py ADDED
@@ -0,0 +1,1261 @@
1
+ """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
2
+ IMPLEMENTATION: HARMONY SEARCH
3
+ """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
4
+
5
+ """
6
+ BACKGROUND - HARMONY SEARCH
7
+
8
+ Initialization: The algorithm starts by initializing a population of candidate solutions, called "harmonies."
9
+ Each harmony represents a potential solution to the optimization problem.
10
+
11
+ Improvisation: Similar to musicians trying out different combinations of notes, Harmony Search generates new
12
+ solutions by combining elements from existing harmonies. This is done through a process called "harmony memory
13
+ consideration."
14
+
15
+ Evaluation: Each newly generated harmony is evaluated based on a fitness function, which measures how
16
+ good the solution is in terms of solving the optimization problem.
17
+
18
+ Updating Harmony Memory: The best harmonies are selected to update the harmony memory, replacing the worst
19
+ harmonies if they are better.
20
+
21
+ Memory Consideration: During the improvisation process, the algorithm considers both the current harmonies
22
+ and the memory of the best solutions found so far to guide the search towards better solutions.
23
+
24
+ Pitch Adjustment: Similar to how musicians adjust their notes to achieve harmony, Harmony Search introduces
25
+ randomness by adjusting certain elements (parameters) of the harmonies. This helps in exploring different
26
+ regions of the search space.
27
+
28
+ Termination: The process continues for a certain number of iterations or until a termination criterion is
29
+ met (e.g., a satisfactory solution is found).
30
+
31
+ """
32
+
33
+ ''' ---------------------------------------------------------- '''
34
+ ''' LIBRARIES '''
35
+ ''' ---------------------------------------------------------- '''
36
+ #from search import*
37
+ import matplotlib.pyplot as plt
38
+ import datetime
39
+ import math
40
+ import pandas as pd
41
+ try:
42
+ from .search import*
43
+ except ImportError:
44
+ from search import*
45
+
46
+ ''' ---------------------------------------------------------- '''
47
+ ''' CONSTANTS '''
48
+ ''' ---------------------------------------------------------- '''
49
+
50
+ sol_keys = ['asvars', 'isvars', 'randvars', 'bcvars', 'corvars', 'bctrans', 'cor']
51
+
52
+ ''' ---------------------------------------------------------- '''
53
+ ''' CLASS FOR HARMONY SEARCH (HS) '''
54
+ ''' ---------------------------------------------------------- '''
55
+ class HarmonySearch(Search):
56
+ # {
57
+ """ Docstring """
58
+
59
+ # ===================
60
+ # CLASS PARAMETERS
61
+ # ===================
62
+
63
+ """
64
+ int mem_size: Harmony memory size / Defaults to 10.
65
+ int min_classes: Minimum number of latent classes. Defaults to 1
66
+ int max_classes: Maximum number of latent classes. Defaults to 5
67
+ float min_harm: Minimum harmony memory consideration rate / Defaults to 0.6.
68
+ float max_harm: Maximum harmony memory consideration rate / Defaults to 0.9.
69
+ float max_pitch: Maximum pitch adjustment rate / Defaults to 0.85
70
+ float min_pitch: Minimum pitch adjustment / Defaults to 0.3
71
+ int maxiter: Maximum iteratioms / Defaults to 30.
72
+ float prop_local: Proportion of iterations without local search / Defaults to 0.8.
73
+ int threshold: Threshold to compare new solution with worst solution / Defaults to 15
74
+
75
+ bool termination_override: termination flag that overrides the default / Defaults to False.
76
+ If true, the search will run for each number of latent classes
77
+ between min_classes and max_classes
78
+
79
+ iter_prop: Proportion of maxiter after which local search is initiated / float
80
+ """
81
+
82
+ # =======================
83
+ # CLASS FUNCTIONS
84
+ # =======================
85
+
86
+ """
87
+ 1. set_control_parameters()
88
+ 2. create_opposite_solution(self, sol);
89
+ 3. initialize_memory(self, nb_sols);
90
+ 4. build_solution(self, memory, prop);
91
+ 5. pitch_adjustment(self, sol, pitch);
92
+ 6. get_best_features(self, memory);
93
+ 7. local_search(self, improved_harmony, iter, pitch);
94
+ 8. improvise(self);
95
+ 9. run(self, latent=False);
96
+ 10. sort_memory(self, mem);
97
+ 11. insert_solution(self, solution):
98
+ """
99
+
100
+ ''' ---------------------------------------------------------- '''
101
+ ''' Function. Constructor '''
102
+ ''' ---------------------------------------------------------- '''
103
+ def set_control_parameters(self, max_harm=0.9, min_harm=0.6, max_pitch=0.85, min_pitch=0.3,
104
+ max_mem=10, maxiter=30, threshold=15, prop_local=0.8, generate_plots=True):
105
+ # {
106
+ self.max_harm = max_harm # Maximum Harmony Memory Considering Rate / float
107
+ self.min_harm = min_harm # Minimum Harmony Memory Considering Rate / float
108
+ self.max_pitch = max_pitch # Maximum Pitch Adjusting Rate / float
109
+ self.min_pitch = min_pitch # Minimum Pitch Adjusting Rate / float
110
+ self.max_mem = max_mem # Harmony memory size / int
111
+ self.maxiter = maxiter # Maximum number of iterations / int
112
+ self.threshold = threshold # Convergence threshold /float
113
+ self.prop_local = prop_local # Proportion of maxiter
114
+ self.perform_local = int(self.prop_local * self.maxiter) # When to apply local search
115
+ self.generate_plots = generate_plots
116
+ # }
117
+
118
+ ''' ---------------------------------------------------------- '''
119
+ ''' Function. Constructor '''
120
+ ''' ---------------------------------------------------------- '''
121
+ def __init__(self, param: Parameters):
122
+ # {
123
+ super().__init__(param) # Call base class constructor
124
+ self.set_control_parameters() # Assume default options - hence no inputs here
125
+ self.pitch = self.max_pitch
126
+ self.memory = [] # List of solutions is empty
127
+ self.all_solutions = [] # Avoid generating same solution twice
128
+
129
+ self.results_file = open("results.txt", "w") # File to output results
130
+ self.progress_file = open("progress.txt", "w") # File to output convergence information
131
+ # }
132
+
133
+ ''' ---------------------------------------------------------------- '''
134
+ ''' Function '''
135
+ ''' ---------------------------------------------------------------- '''
136
+ def sort_memory(self, mem):
137
+ # {
138
+ if self.param.nb_crit > 1:
139
+ mem = self.non_dominant_sorting(mem)
140
+ else:
141
+ mem = sorted(mem, key=lambda sol: sol.obj[0])
142
+ return mem
143
+ # }
144
+
145
+ ''' ---------------------------------------------------------------- '''
146
+ ''' Function '''
147
+ ''' ---------------------------------------------------------------- '''
148
+ def create_opposite_solution(self, sol):
149
+ # {
150
+ opp_sol = self.generate_solution()
151
+ for key in sol_keys: # Iterate through variable types
152
+ # {
153
+ skip = (not opp_sol[key] or isinstance(opp_sol[key], bool) or getattr(self.param, 'ps_' + key))
154
+ if skip:
155
+ continue # Skip current loop
156
+ else:
157
+ # {
158
+ opp_sol[key] = [v for v in opp_sol[key] if v not in sol[key]] # Filter out elements in sol[key]
159
+ if self.param.ps_intercept is None:
160
+ opp_sol['asc_ind'] = not sol['asc_ind']
161
+ # }
162
+ # }
163
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
164
+ if self.param.avail_rvars:
165
+ opp_sol['randvars'] = {k: self.param.generator.choice(self.param.distr)
166
+ for k in opp_sol['randvars'] if k in opp_sol['asvars']}
167
+ else:
168
+ opp_sol['randvars'] = {}
169
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
170
+ opp_sol['corvars'] = [corvar for corvar in opp_sol['corvars'] if corvar in opp_sol['randvars']]
171
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
172
+ if self.param.avail_bcvars:
173
+ opp_sol['bcvars'] = [bcvar for bcvar in opp_sol['bcvars']
174
+ if bcvar in opp_sol['asvars'] and bcvar not in opp_sol['corvars']]
175
+ else:
176
+ opp_sol['bcvars'] = []
177
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
178
+ self.revise_solution('class_params_spec', opp_sol, sol)
179
+ self.revise_solution('member_params_spec', opp_sol, sol)
180
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
181
+ # }
182
+
183
+ ''' ---------------------------------------------------------------- '''
184
+ ''' Function. Initialization of harmony search memory '''
185
+ ''' ---------------------------------------------------------------- '''
186
+ def initialize_memory(self, nb_sols):
187
+ # {
188
+ """ This function initializes the harmony memory and opposite
189
+ harmony memory with unique random solutions. The harmony memory
190
+ stores initial solutions, while the opposite harmony memory
191
+ stores solutions that include variables not included in the
192
+ harmony memory. If the generated solution converges, it's added
193
+ to the harmony memory. Otherwise, the function generates an
194
+ "opposite" solution and, if it converges, adds it to the
195
+ opposite harmony memory.
196
+ """
197
+
198
+ mem, opp_mem = [], []
199
+ for counter in range(30000):
200
+ # {
201
+ sol = self.generate_solution() # Generated solution
202
+ sol, converged = self.evaluate_solution(sol)
203
+ if converged: mem.append(sol) # Add new solution to list
204
+
205
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
206
+ # Create opposite solution that has non_included variables
207
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
208
+ opp_sol = self.create_opposite_solution(sol)
209
+ opp_sol, converged = self.evaluate_solution(opp_sol)
210
+ if converged: opp_mem.append(opp_sol)
211
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
212
+
213
+ mem += opp_mem # Aggregate solutions
214
+ mem = get_unique(mem, 0) # Keep unique solutions only - Compare by first objective
215
+
216
+ # QUERY: WHY NOT FILTER BY sol.obj[1] AS WELL?
217
+
218
+ mem = [sol for sol in mem if abs(sol.obj[0]) < BOUND] # Filter out poor solutions
219
+ if len(mem) >= nb_sols:
220
+ return mem[:nb_sols] # Exit and return list of solutions
221
+ # }
222
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
223
+ return mem # Failed to generate required number of solutions
224
+ # }
225
+
226
+ ''' ---------------------------------------------------------- '''
227
+ ''' Function. Build new solution using Harmony Memory '''
228
+ ''' A new solution, could either be built from an existing one '''
229
+ ''' or constructed randomly. '''
230
+ ''' ---------------------------------------------------------- '''
231
+ def build_solution(self, memory, prop):
232
+ # {
233
+ """ This function decides whether to build a new solution from an existing solution
234
+ in the harmony memory or to generate a completely new solution, based on a random number and the
235
+ Harmony Memory Consideration Rate (HMCR). If the random number is less than or equal to prop,
236
+ it selects a proportion of the features from a randomly chosen existing solution to build the new solution.
237
+ Otherwise, it generates a completely new solution """
238
+
239
+
240
+ bin = [0,1] # Binary values
241
+ prob = [1-prop, prop] # Range
242
+ new_sol = Solution(nb_crit=self.nb_crit) # Create a new solution object
243
+
244
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
245
+ # IS THIS NECESSARY?
246
+ '''fronts, pareto = None, None
247
+ if nb_crit > 1: # {
248
+ memory = self.non_dominant_sorting(memory)
249
+ fronts = self.get_fronts(memory)
250
+ pareto = self.get_pareto(fronts, memory)
251
+ # }'''
252
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
253
+ if self.param.generator.rand() > prop:
254
+ new_sol = self.generate_solution() # Generate a new solution
255
+ else:
256
+ # {
257
+ choice = self.param.generator.choice(len(memory)) # Choose one of the member solutions
258
+ chosen_sol = memory[choice] # Define reference to the chosen member solution
259
+
260
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
261
+ # OPTIONAL CODE.
262
+ # size = len(chosen_sol['asvars'])
263
+ # new_asvars_index = self.param.generator.choice(bin, size=size, p=prob)
264
+ # new_asvars = [i for (i, v) in zip(chosen_sol['asvars'], new_asvars_index) if v]
265
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
266
+
267
+ # Randomly select a subset of the variables from the chosen solution
268
+ size = int((len(chosen_sol['asvars'])) * prop)
269
+ new_asvars = list(self.param.generator.choice(chosen_sol['asvars'], size=size, replace=False))
270
+ n_asvars = sorted(list(set().union(new_asvars, self.param.ps_asvars)))
271
+ new_asvars = self.remove_redundant_asvars(n_asvars, self.param.trans_asvars, self.param.asvarnames)
272
+ new_sol['asvars'] = new_asvars
273
+
274
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
275
+ # Randomly select a subset of the variables from the chosen solution
276
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
277
+ size = int((len(chosen_sol['isvars'])) * prop)
278
+ new_isvars = list(self.param.generator.choice(chosen_sol['isvars'], size=size, replace=False))
279
+ new_isvars = sorted(list(set().union(new_isvars, self.param.ps_isvars)))
280
+ new_sol['isvars'] = new_isvars
281
+
282
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
283
+ # Include variables in new solution based on the chosen solution
284
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
285
+ new_randvars = {k: v for k, v in chosen_sol['randvars'].items() if k in new_asvars}
286
+ new_sol['randvars'] = new_randvars
287
+
288
+ new_bcvars = [var for var in chosen_sol['bcvars']
289
+ if var in new_asvars and var not in self.param.ps_corvars]
290
+ new_sol['bcvars'] = new_bcvars
291
+
292
+ new_corvars = chosen_sol['corvars']
293
+ if new_corvars:
294
+ new_corvars = [var for var in chosen_sol['corvars']
295
+ if var in new_randvars.keys() and var not in new_bcvars]
296
+ new_sol['corvars'] = new_corvars
297
+
298
+ # Take fit_intercept from chosen solution
299
+ new_sol['asc_ind'] = chosen_sol['asc_ind']
300
+
301
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
302
+ if chosen_sol['class_params_spec'] is not None:
303
+ # {
304
+ class_params_spec = copy.deepcopy(chosen_sol['class_params_spec'])
305
+ for ii, class_params in enumerate(class_params_spec):
306
+ # {
307
+ class_params_index = self.param.generator.choice(bin, size=len(class_params), p=prob)
308
+ class_params_spec[ii] = np.array([i for (i, v) in zip(class_params, class_params_index) if v],
309
+ dtype=class_params.dtype)
310
+ # }
311
+ new_sol['class_params_spec'] = class_params_spec
312
+ # }
313
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
314
+ if chosen_sol['member_params_spec'] is not None:
315
+ # {
316
+ member_params_spec = copy.deepcopy(chosen_sol['member_params_spec'])
317
+ for ii, member_params in enumerate(member_params_spec):
318
+ # {
319
+ member_params_index = self.param.generator.choice(bin, size=len(member_params), p=prob)
320
+ member_params_spec[ii] = np.array([i for (i, v) in zip(member_params, member_params_index) if v],
321
+ dtype=member_params.dtype)
322
+ # }
323
+ new_sol['member_params_spec'] = member_params_spec
324
+ # }
325
+ # }
326
+
327
+ return new_sol
328
+ # }
329
+
330
+ ''' ---------------------------------------------------------- '''
331
+ ''' Function '''
332
+ ''' ---------------------------------------------------------- '''
333
+ def remove_non_unique_solutions(self):
334
+ # {
335
+ seen_tuple = set() # Create an empty set
336
+ new_memory = list() # Create an empty list
337
+ crit = self.param.criterions[:self.nb_crit]
338
+ for sol in self.memory:
339
+ # {
340
+ sol_tuple = tuple([sol[crit[0]], sol[crit[1]]])
341
+ if sol_tuple not in seen_tuple:
342
+ # {
343
+ seen_tuple.add(sol_tuple) # Revise what has been seen
344
+ new_memory.append(sol) # Update list of unique solutions
345
+ # }
346
+ # }
347
+ self.memory = new_memory
348
+ # }
349
+
350
+ ''' ------------------------------------------------------------ '''
351
+ ''' Function. Insert solution and filter out non-unique solutions'''
352
+ ''' ------------------------------------------------------------ '''
353
+ def insert_solution(self, solution):
354
+ # {
355
+ self.memory.append(copy.deepcopy(solution))
356
+ self.remove_non_unique_solutions()
357
+ self.memory = self.sort_memory(self.memory)
358
+ # }
359
+
360
+ ''' ---------------------------------------------------------- '''
361
+ ''' Function. Performs the pitch adjustment operation to '''
362
+ ''' fine-tune a given solution. The process includes adding '''
363
+ ''' new features or removing existing ones based on a binary '''
364
+ ''' indicator. The resulting solution is evaluated and inserted'''
365
+ ''' The solutions in memory are then filtered '''
366
+ ''' ---------------------------------------------------------- '''
367
+ def pitch_adjustment(self, sol, pitch):
368
+ # {
369
+ # QUERY. IS DEEP COPY REQUIRED?
370
+ adj = copy.deepcopy(sol) # Adjusted solution
371
+
372
+ # pitch adjustment: add/remove as variables
373
+ if self.param.generator.rand() <= pitch: adj = self.perturb_asfeature(sol)
374
+ # pitch adjustment: add|remove is variables
375
+ if self.param.generator.rand() <= pitch: adj = self.perturb_isfeature(adj)
376
+ # pitch adjustment: add|remove random variable
377
+ if self.param.generator.rand() <= pitch: adj = self.perturb_randfeature(adj)
378
+
379
+ if self.param.generator.rand() <= pitch: adj = self.change_distribution(adj)
380
+
381
+ # pitch adjustment: add|remove bc variables
382
+ if self.param.generator.rand() <= pitch: adj = self.perturb_bcfeature(adj, pitch)
383
+ # Pitch adjustment: add|remove cor variables
384
+ if self.param.generator.rand() <= pitch: adj = self.perturb_corfeature(adj)
385
+ # Pitch adjustment: add|remove class param variables
386
+ if self.param.generator.rand() <= pitch: adj = self.perturb_class_paramfeature(adj)
387
+ # Pitch adjustment: add|remove member param variables
388
+ if self.param.generator.rand() <= pitch: adj = self.perturb_member_paramfeature(adj)
389
+
390
+ adj, converged = self.evaluate_solution(adj)
391
+ return adj, converged
392
+ # }
393
+
394
+ ''' ---------------------------------------------------------- '''
395
+ ''' Function. Extracts the best features '''
396
+ ''' ---------------------------------------------------------- '''
397
+ def get_best_features(self, memory):
398
+ # {
399
+ soln = self.find_best_sol(memory)
400
+
401
+ # Copy necessary values from soln dictionary
402
+ best_asvars = soln['asvars'].copy()
403
+ best_isvars = soln['isvars'].copy()
404
+ best_randvars = soln['randvars'].copy()
405
+ best_bcvars = soln['bcvars'].copy()
406
+ best_corvars = soln['corvars'].copy()
407
+ asc_ind = soln['asc_ind']
408
+ best_class_params_spec = soln['class_params_spec'].copy() if soln['class_params_spec'] is not None else None
409
+ best_member_params_spec = soln['member_params_spec'].copy() if soln['member_params_spec'] is not None else None
410
+
411
+ # Return a tuple containing eight things
412
+ return (best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, asc_ind, best_class_params_spec,
413
+ best_member_params_spec)
414
+ # }
415
+
416
+ ''' ---------------------------------------------------------- '''
417
+ ''' Function. Used in local_search_1 to local_search_10 '''
418
+ ''' ---------------------------------------------------------- '''
419
+ def make_evaluate_insert(self, _asvars, _isvars, _randvars, _bcvars, _corvars, _asc_ind,
420
+ _class_params_spec, _member_params_spec):
421
+ # {
422
+ solution = Solution(nb_crit=self.nb_crit, asvars=_asvars, isvars=_isvars, randvars=_randvars, bcvars=_bcvars,
423
+ corvars=_corvars, asc_ind=_asc_ind, class_params_spec=_class_params_spec,
424
+ member_params_spec=_member_params_spec)
425
+
426
+ revised_solution, converged = self.evaluate_solution(solution)
427
+ if converged:
428
+ self.insert_solution(revised_solution)
429
+ # }
430
+
431
+ ''' ---------------------------------------------------------- '''
432
+ ''' Function. Apply local search to improve a solution '''
433
+ ''' ---------------------------------------------------------- '''
434
+ # Check whether changing a coefficient distribution improves the kpi
435
+ def make_change_1(self, candidate):
436
+ # {
437
+ # Identify the best solution features
438
+ best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, \
439
+ asc_ind, best_class_params_spec, best_member_params_spec = self.get_best_features(candidate)
440
+
441
+ # Make changes
442
+ solution = Solution(nb_crit=self.nb_crit, asvars=best_asvars, isvars=best_isvars, randvars=best_randvars, bcvars=best_bcvars,
443
+ corvars=best_corvars, asc_ind=asc_ind, class_params_spec=best_class_params_spec,
444
+ member_params_spec=best_member_params_spec)
445
+
446
+ revised_solution = self.change_distribution(solution) # Make a change
447
+
448
+ # Revise the following dictionary and lists
449
+ best_randvars = {key: val for key, val in best_randvars.items() if key in best_asvars and val != 'f'}
450
+ best_bcvars = [var for var in best_bcvars if var in best_asvars and var not in self.param.ps_corvars]
451
+ best_corvars = [var for var in best_randvars.keys() if var not in best_bcvars]
452
+
453
+ # Make a solution, evaluate it, and insert if converged
454
+ self.make_evaluate_insert(best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, asc_ind,
455
+ best_class_params_spec, best_member_params_spec)
456
+ # }
457
+
458
+ # Check if having a full covariance matrix leads to an improved BIC
459
+ def make_change_2(self, candidate):
460
+ # {
461
+ # Identify the best solution features
462
+ best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, \
463
+ asc_ind, best_class_params_spec, best_member_params_spec = self.get_best_features(candidate)
464
+
465
+ # Make changes
466
+ best_bcvars = [var for var in best_asvars if var in self.param.ps_bcvars]
467
+ if self.param.ps_cor is None or self.param.ps_cor:
468
+ best_corvars = [var for var in best_randvars if var not in best_bcvars]
469
+ else:
470
+ best_corvars.clear()
471
+
472
+ # Make a solution, evaluate it, and insert if converged
473
+ self.make_evaluate_insert(best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, asc_ind,
474
+ best_class_params_spec, best_member_params_spec)
475
+ # }
476
+
477
+ # Check if having all the variables transformed leads to an improvement in BIC
478
+ def make_change_3(self, candidate):
479
+ # {
480
+ # Identify the best solution features
481
+ best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, \
482
+ asc_ind, best_class_params_spec, best_member_params_spec = self.get_best_features(candidate)
483
+
484
+ # Make changes
485
+ if self.param.ps_bctrans is None or self.param.ps_bctrans:
486
+ best_bcvars = [var for var in best_asvars if var not in self.param.ps_corvars]
487
+ else:
488
+ best_bcvars.clear()
489
+
490
+ best_corvars = [var for var in best_randvars.keys() if var not in best_bcvars]
491
+
492
+ # Make a solution, evaluate it, and insert if converged
493
+ self.make_evaluate_insert(best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, asc_ind,
494
+ best_class_params_spec, best_member_params_spec)
495
+ # }
496
+
497
+ def make_change_4(self, candidate):
498
+ # {
499
+ # Identify the best solution features
500
+ best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, \
501
+ asc_ind, best_class_params_spec, best_member_params_spec = self.get_best_features(candidate)
502
+
503
+ if len(best_asvars) < len(self.param.asvarnames):
504
+ # {
505
+ solution = Solution(nb_crit=self.nb_crit, asvars=best_asvars, isvars=best_isvars,
506
+ randvars=best_randvars, bcvars=best_bcvars,
507
+ corvars=best_corvars, asc_ind=asc_ind,
508
+ class_params_spec=best_class_params_spec,
509
+ member_params_spec=best_member_params_spec)
510
+
511
+ solution = self.add_asfeature(solution)
512
+ revised_solution, converged = self.evaluate_solution(solution)
513
+ if converged: self.insert_solution(revised_solution)
514
+ # }
515
+ # }
516
+
517
+
518
+ def make_change_5(self, candidate):
519
+ # {
520
+ # Identify the best solution features
521
+ best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, \
522
+ asc_ind, best_class_params_spec, best_member_params_spec = self.get_best_features(candidate)
523
+
524
+ solution = Solution(nb_crit=self.nb_crit, asvars=best_asvars, isvars=best_isvars,
525
+ randvars=best_randvars, bcvars=best_bcvars, corvars=best_corvars, asc_ind=asc_ind,
526
+ class_params_spec=best_class_params_spec, member_params_spec=best_member_params_spec)
527
+
528
+ solution = self.add_isfeature(solution)
529
+ revised_solution, converged = self.evaluate_solution(solution)
530
+ if converged: self.insert_solution(revised_solution)
531
+ # }
532
+
533
+ def make_change_6(self, candidate):
534
+ # {
535
+ # Identify the best solution features
536
+ best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, \
537
+ asc_ind, best_class_params_spec, best_member_params_spec = self.get_best_features(candidate)
538
+
539
+ solution = Solution(nb_crit=self.nb_crit, asvars=best_asvars, isvars=best_isvars, randvars=best_randvars,
540
+ bcvars=best_bcvars, corvars=best_corvars, asc_ind=asc_ind,
541
+ class_params_spec=best_class_params_spec, member_params_spec=best_member_params_spec)
542
+
543
+ if self.param.avail_bcvars: # {
544
+ solution = self.perturb_bcfeature(solution, self.pitch)
545
+ revised_solution, converged = self.evaluate_solution(solution)
546
+ if converged: self.insert_solution(revised_solution)
547
+ # }
548
+ # }
549
+
550
+ def make_change_7(self, candidate):
551
+ # {
552
+ # Identify the best solution features
553
+ best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, \
554
+ asc_ind, best_class_params_spec, best_member_params_spec = self.get_best_features(candidate)
555
+
556
+ solution = Solution(nb_crit=self.nb_crit, asvars=best_asvars, isvars=best_isvars, randvars=best_randvars, bcvars=best_bcvars,
557
+ corvars=best_corvars, asc_ind=asc_ind, class_params_spec=best_class_params_spec,
558
+ member_params_spec=best_member_params_spec)
559
+
560
+ if self.param.avail_rvars:
561
+ # {
562
+ solution = self.perturb_corfeature(solution)
563
+ revised_solution, converged = self.evaluate_solution(solution)
564
+ if converged:
565
+ self.insert_solution(revised_solution)
566
+
567
+ revised_solution = self.perturb_randfeature(solution)
568
+ revised_solution, converged = self.evaluate_solution(revised_solution)
569
+ if converged:
570
+ self.insert_solution(revised_solution)
571
+ # }
572
+ # }
573
+
574
+ # Check if changing coefficient distributions improves the solution BIC
575
+ def make_change_8(self, candidate):
576
+ # {
577
+ # Identify the best solution features
578
+ best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, \
579
+ asc_ind, best_class_params_spec, best_member_params_spec = self.get_best_features(candidate)
580
+
581
+ for var in best_randvars: # {
582
+ if var not in self.param.ps_randvars:
583
+ rm_dist = [distr for distr in self.param.distr if distr != best_randvars[var]]
584
+ best_randvars[var] = self.param.generator.choice(rm_dist)
585
+ # }
586
+ best_randvars = {key: val for key, val in best_randvars.items() if key in best_asvars and val != 'f'}
587
+ best_bcvars = [var for var in best_bcvars if var in best_asvars and var not in self.param.ps_corvars]
588
+
589
+ if self.param.ps_cor is None or self.param.ps_cor:
590
+ best_corvars = [var for var in best_randvars.keys() if var not in best_bcvars]
591
+ else:
592
+ best_corvars = []
593
+
594
+ if len(best_corvars) < 2:
595
+ best_corvars = []
596
+
597
+ solution = Solution(nb_crit=self.nb_crit, asvars=best_asvars, isvars=best_isvars,
598
+ randvars=best_randvars, bcvars=best_bcvars, corvars=best_corvars,
599
+ asc_ind=asc_ind, class_params_spec=best_class_params_spec,
600
+ member_params_spec=best_member_params_spec)
601
+ revised_solution, converged = self.evaluate_solution(solution)
602
+
603
+ if converged:
604
+ self.insert_solution(revised_solution)
605
+ # }
606
+
607
+ def make_change_9(self, candidate):
608
+ # {
609
+ # Identify the best solution features
610
+ best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, \
611
+ asc_ind, best_class_params_spec, best_member_params_spec = self.get_best_features(candidate)
612
+
613
+
614
+ solution = Solution(nb_crit=self.nb_crit, asvars=best_asvars, isvars=best_isvars,
615
+ randvars=best_randvars, bcvars=best_bcvars, corvars=best_corvars, asc_ind=asc_ind,
616
+ class_params_spec=best_class_params_spec, member_params_spec=best_member_params_spec)
617
+
618
+ if solution['class_params_spec'] is not None:
619
+ # {
620
+ revised_solution = self.perturb_class_paramfeature(solution)
621
+ revised_solution, converged = self.evaluate_solution(revised_solution)
622
+ if converged:
623
+ self.insert_solution(revised_solution)
624
+ # }
625
+ # }
626
+
627
+ def make_change_10(self, candidate):
628
+ # {
629
+ # Identify the best solution features
630
+ best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, \
631
+ asc_ind, best_class_params_spec, best_member_params_spec = self.get_best_features(candidate)
632
+
633
+ solution = Solution(nb_crit=self.nb_crit, asvars=best_asvars, isvars=best_isvars,
634
+ randvars=best_randvars, bcvars=best_bcvars, corvars=best_corvars, asc_ind=asc_ind,
635
+ class_params_spec=best_class_params_spec, member_params_spec=best_member_params_spec)
636
+
637
+ if solution['member_params_spec'] is not None:
638
+ # {
639
+ revised_solution = self.perturb_member_paramfeature(solution)
640
+ revised_solution, converged = self.evaluate_solution(revised_solution)
641
+ if converged: self.insert_solution(revised_solution)
642
+ # }
643
+ # }
644
+
645
+ def local_search(self):
646
+ # {
647
+ # Identify candidate solutions
648
+ candidate = [sol for sol in self.memory if abs(sol.obj[0]) < BOUND]
649
+
650
+ # TODO
651
+ # }
652
+
653
+ ''' ---------------------------------------------------------- '''
654
+ ''' Function '''
655
+ ''' ---------------------------------------------------------- '''
656
+ def log_convergence(self, memory):
657
+ # {
658
+ crit = self.param.criterions[:self.nb_crit]
659
+
660
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
661
+ # Filter out poor solutions:
662
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
663
+ filtered_memory = [sol for sol in memory if abs(sol[crit[0]]) < BOUND and abs(sol[crit[1]]) < BOUND]
664
+ # OR,
665
+ # filtered_memory = [sol for sol in memory if abs(sol.obj[0]) < BOUND and abs(sol.obj[1]) < BOUND]
666
+
667
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
668
+ # Sort the new list of solutions by 'sol_num':
669
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
670
+ filtered_memory = sorted(filtered_memory, key=lambda sol: sol['sol_num'])
671
+
672
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~
673
+ # Record the best obj val
674
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~
675
+ best_val = self.get_best_val(self.param.criterions, filtered_memory)
676
+ all_val = self.get_all_val(self.param.criterions, filtered_memory)
677
+ for i in range(self.nb_crit):
678
+ logger.debug(f"Best points (obj {i}): {best_val[i]}") # Log the ith value
679
+
680
+ return all_val, best_val
681
+ # }
682
+
683
+ ''' ---------------------------------------------------------- '''
684
+ ''' Function Conduct harmony memory consideration '''
685
+ ''' pitch adjustment, and local search '''
686
+ ''' This function tracks the progress of the optimization '''
687
+ ''' process by recording the score of the best and current '''
688
+ ''' solutions at each iteration '''
689
+ ''' ---------------------------------------------------------- '''
690
+ def improvise(self):
691
+ # {
692
+ best, current = [], []
693
+ for iter in range(self.maxiter):
694
+ # {
695
+ # Compute consideration rate and pitch value
696
+ # This code introduces oscillations (a.k.a., variations) based on the iteration number.
697
+ # The result is scaled by the sine function only when its value is non-negative.
698
+ sine_iter = max(0, np.sign(math.sin(iter)))
699
+ self.harm_rate = (self.min_harm + ((self.max_harm - self.min_harm) / self.maxiter) * iter) * sine_iter
700
+ self.pitch = (self.min_pitch + ((self.max_pitch - self.min_pitch) / self.maxiter) * iter) * sine_iter
701
+
702
+ new_sol = self.build_solution(self.memory, self.harm_rate) # Create a single new solution and perform an adjustment
703
+ curr_sol, converged = self.pitch_adjustment(new_sol, self.pitch) # Perform additional perturbations
704
+ if converged:
705
+ # {
706
+ self.insert_solution(curr_sol)
707
+
708
+ #if iter > int(self.prop_local * self.maxiter):
709
+ # {
710
+ # Run local search
711
+ #best_sol = self.memory[0]
712
+ #best.append(best_sol.obj[0])
713
+ #current.append(curr_sol.obj[0])
714
+ # }
715
+ # }
716
+ # }
717
+
718
+ all_val, obj_val = self.log_convergence(self.memory)
719
+ if self.generate_plots:
720
+ self.plot_results(self.memory, all_val, obj_val)
721
+ # }
722
+
723
+ ''' ---------------------------------------------------------- '''
724
+ ''' Function. Discard non-convergent solutions '''
725
+ ''' ---------------------------------------------------------- '''
726
+ def screen_solutions(self, solutions):
727
+ # {
728
+ feasible_solutions = []
729
+ if solutions is not None:
730
+ # {
731
+ for sol in solutions:
732
+ # {
733
+ new_sol = copy.deepcopy(sol)
734
+ new_sol = self.increase_sol_by_one_class(new_sol)
735
+ new_sol.pop('class_num') # Remove 'class_num'
736
+ new_sol, converged = self.evaluate_solution(new_sol)
737
+ if converged:
738
+ feasible_solutions.append(new_sol)
739
+ # }
740
+ # }
741
+ return feasible_solutions
742
+ # }
743
+
744
+ ''' ---------------------------------------------------------- '''
745
+ ''' Function. Code used in "self.run_search" '''
746
+ ''' ---------------------------------------------------------- '''
747
+ def extract_parameter(self):
748
+ # {
749
+ avail, avail_latent = self.param.avail, self.param.avail_latent
750
+ weights = self.param.weights
751
+ alt_var = self.param.alt_var
752
+ choice_id = self.param.choice_id
753
+ ind_id = self.param.ind_id
754
+
755
+ if self.nb_crit > 1:
756
+ # {
757
+ if self.param.avail is not None:
758
+ avail = np.row_stack((self.param.avail, self.param.test_avail))
759
+
760
+ if self.param.avail_latent is not None:
761
+ # {
762
+ avail_latent = make_list(None, self.param.num_classes) # i.e., [None] * self.param.num_classes
763
+ for ii, avail_latent_ii in enumerate(self.param.avail_latent):
764
+ # {
765
+ if avail_latent_ii is not None:
766
+ avail_latent[ii] = np.row_stack((avail_latent_ii, self.param.test_avail_latent[ii]))
767
+ # }
768
+ # }
769
+
770
+ if self.param.weights is not None:
771
+ weights = np.concatenate((self.param.weights, self.param.test_weight_var))
772
+
773
+ if self.param.alt_var is not None:
774
+ alt_var = np.concatenate((self.param.alt_var, self.param.test_alt_var))
775
+
776
+ if self.param.choice_id is not None:
777
+ choice_id = np.concatenate((self.param.choice_id, self.param.test_choice_id))
778
+
779
+ if self.param.ind_id is not None:
780
+ ind_id = np.concatenate((self.param.ind_id, self.param.test_ind_id))
781
+ # }
782
+ return avail, weights, alt_var, choice_id, ind_id, avail_latent
783
+ # }
784
+
785
+ ''' ---------------------------------------------------------- '''
786
+ ''' Function. '''
787
+ ''' ---------------------------------------------------------- '''
788
+ def extract_from_sol(self, sol):
789
+ # {
790
+ asvarnames, isvarnames, randvars, bcvars, corvars, intercept, \
791
+ class_params_spec, member_params_spec = self.get_components(sol)
792
+
793
+ # Revise varnames
794
+ if self.param.latent_class:
795
+ varnames = np.concatenate(class_params_spec + member_params_spec + [isvarnames])
796
+ varnames = np.unique(varnames)
797
+ else:
798
+ varnames = asvarnames + isvarnames
799
+
800
+ # Delete '_inter' bug fix
801
+ if '_inter' in varnames:
802
+ varnames = np.delete(varnames, np.argwhere(varnames == '_inter'))
803
+
804
+ return varnames, asvarnames, isvarnames, randvars, bcvars, corvars, \
805
+ intercept, class_params_spec, member_params_spec
806
+ # }
807
+
808
+ # call if self.param.multi_objective?
809
+ def test_best_solution(self, best_sol):
810
+ # {
811
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
812
+ # Sort memory and extract features
813
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
814
+ best_varnames, best_asvarnames, best_isvarnames, best_randvars, best_bcvars, best_corvars, \
815
+ best_intercept, best_class_params_spec, best_member_params_spec = self.extract_from_sol(best_sol)
816
+
817
+ avail_all, weights_all, alt_var_all, choice_id_all, ind_id_all, avail_latent_all = self.extract_parameter()
818
+
819
+ df_all = pd.concat([self.param.df, self.param.df_test], ignore_index=True)
820
+ y = self.param.choices + self.param.test_choices
821
+ X = df_all[best_varnames]
822
+
823
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
824
+ # Define appropriate model and fit coefficients
825
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
826
+ if bool(best_randvars):
827
+ # {
828
+ if self.param.latent_class:
829
+ """
830
+ Note template: search::fit_lcmm(X, y, varnames, isvars, class_params_spec, member_params_spec, num_classes,
831
+ alts, ids, panels, bcvars, randvars, corvars, maxiter, gtol, avail, weights)
832
+ """
833
+ model = self.fit_lcmm(X, y, best_varnames, best_isvarnames, best_class_params_spec,
834
+ best_member_params_spec, self.param.num_classes, alt_var_all, choice_id_all,
835
+ ind_id_all, best_bcvars, best_randvars, best_corvars,
836
+ self.param.maxiter, self.param.gtol, avail_all, weights_all)
837
+ else:
838
+ """
839
+ Note template: search::fit_mxl(X, y, varnames, alts, isvars, transvars, ids, panels, randvars, corvars,
840
+ fit_intercept, n_draws, weights, avail, base_alt, maxiter, seed, ftol, gtol, save_fitted_params)
841
+ """
842
+ model = self.fit_mxl(X, y, best_varnames, alt_var_all, best_isvarnames, best_bcvars,
843
+ choice_id_all, ind_id_all, best_randvars, best_corvars, best_intercept,
844
+ self.param.n_draws, None, None, None, 2000, None, 1e-6, 1e-6, False)
845
+ # }
846
+ # }
847
+ else:
848
+ # {
849
+ if self.param.latent_class:
850
+ """
851
+ Note template: search::fit_lcm(X, y, varnames, class_params_spec, member_params_spec, num_classes, ids,
852
+ transvars, maxiter, gtol, gtol_membership_func, avail, avail_latent, intercept_opts, weights, seed,
853
+ alts, ftol_lccm, base_alt)
854
+ """
855
+ seed = self.param.generator.randint(2 ** 31 - 1)
856
+ model = self.fit_lcm(X, y, best_varnames, best_class_params_spec, best_member_params_spec,
857
+ self.param.num_classes, choice_id_all, best_bcvars, self.param.maxiter,
858
+ self.param.gtol, self.param.gtol_membership_func, avail_all, avail_latent_all,
859
+ self.param.intercept_opts, weights_all, seed, None, 1e-6, None)
860
+
861
+ else:
862
+ """
863
+ Note template: search::fit_mnl(X, y, varnames, isvars, alts, ids, transvars, fit_intercept,
864
+ weights, avail, base_alt, maxiter, ftol, gtol, seed)
865
+ """
866
+ model = self.fit_mnl(X, y, best_varnames, best_isvarnames, alt_var_all,
867
+ choice_id_all, best_bcvars, best_intercept, None, None, None, 2000, 1e-6, 1e-6, None)
868
+ # }
869
+
870
+ report_model_statistics(self.results_file, model) # Output the model statistics
871
+ # }
872
+
873
+ ''' ---------------------------------------------------------- '''
874
+ ''' Function. '''
875
+ ''' ---------------------------------------------------------- '''
876
+ def log_solutions(self, solutions):
877
+ # {
878
+ if self.nb_crit == 1:
879
+ # {
880
+ all_solutions = sorted(solutions, key=lambda sol: sol.obj[0])
881
+ best_sols = all_solutions[:self.max_mem]
882
+ best_sol = best_sols[0]
883
+ logger.info("Model with best score had {} classes".format(self.max_classes))
884
+ logger.info("Best solution")
885
+ for k, v in best_sol.items():
886
+ logger.info(f"{k}: {v}")
887
+ # }
888
+ else:
889
+ # {
890
+ fronts = self.get_fronts(solutions)
891
+ pareto = self.get_pareto(fronts, solutions)
892
+ all_solutions = self.non_dominant_sorting(solutions)
893
+ logger.info("Models in Pareto front had at most {} classes".format(self.max_classes))
894
+ logger.info("Best models in Pareto front")
895
+ for i, sol in enumerate(pareto):
896
+ # {
897
+ logger.info(f'Best solution - {i}')
898
+ for k, v in sol.items():
899
+ logger.info(f"{k}: {v}")
900
+ # }
901
+ best_sol = all_solutions[0]
902
+ logger.info(f"Best solution with {best_sol['class_num']} classes")
903
+ for k, v in best_sol.items():
904
+ logger.info(f"{k}: {v}")
905
+ # }
906
+ # }
907
+
908
+ ''' ---------------------------------------------------------- '''
909
+ ''' Function. '''
910
+ ''' ---------------------------------------------------------- '''
911
+ def run_search(self, existing_sols=None):
912
+ # {
913
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
914
+ # Combine solutions into one list
915
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
916
+ existing_memory = self.screen_solutions(existing_sols) # Screen out non convergent solutions
917
+ generated_memory = self.initialize_memory(self.max_mem) # Generate some solutions
918
+
919
+ # OPTIONAL: CREATE max(max_mem - existing, 0) NEW SOLUTIONS?
920
+
921
+ init_memory = generated_memory + existing_memory # Aggregate solution lists
922
+ unique_memory = get_unique(init_memory, 0) # Remove duplicate solutions if present
923
+ for sol in unique_memory:
924
+ sol.data['is_initial_sol'] = True # Set solution status
925
+
926
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
927
+ # Sort memory by first objective or by Pareto ranking and retain specified number
928
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
929
+ memory_sorted = self.sort_memory(unique_memory)
930
+ memory = memory_sorted[:self.max_mem]
931
+ self.memory = memory.copy()
932
+
933
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
934
+ # Generate new solutions by combining elements from existing ones
935
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
936
+ self.improvise()
937
+
938
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
939
+ # Copy, Sort & Test
940
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
941
+ memory = self.memory.copy()
942
+ improved_memory = self.sort_memory(memory)
943
+ best_sol = improved_memory[0]
944
+ self.test_best_solution(best_sol)
945
+
946
+ # ~~~~~~~~~~~~~~~~
947
+ # Perform logging
948
+ # ~~~~~~~~~~~~~~~~
949
+ logger.info("Improved harmony: {}".format(improved_memory))
950
+ logger.info("Search ended at: {}".format(str(time.ctime())))
951
+
952
+ return improved_memory
953
+ # }
954
+
955
+
956
+ def run_search_latent(self, override=False):
957
+ # {
958
+ prev, best_model_idx = infinity, 0
959
+ all_solutions, solutions = [], []
960
+
961
+ for q in range(self.min_classes, self.max_classes + 1):
962
+ # {
963
+ self.param.num_classes = q
964
+ self.param.latent_class = False if q == 1 else True
965
+ solutions = self.run_search(existing_sols=all_solutions)
966
+
967
+ # This code iterates over each dictionary sol in the solutions list and updates
968
+ # the value associated with the key 'class_num' to q. The use of a list
969
+ # comprehension avoids the need for an explicit loop.
970
+ [sol.update({'class_num': q}) for sol in solutions]
971
+
972
+ # Aggregate solutions
973
+ all_solutions = all_solutions + solutions
974
+
975
+ if self.param.nb_crit > 1:
976
+ # {
977
+ all_solutions = self.non_dominant_sorting(all_solutions)
978
+ fronts = self.get_fronts(all_solutions)
979
+ pareto = self.get_pareto(fronts, all_solutions)
980
+ self.pareto_front = pareto
981
+
982
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
983
+ # Check if a solution with q classes is in the Pareto front
984
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
985
+ pareto_class_nums = [sol['class_num'] for sol in pareto]
986
+ stop_run = max(pareto_class_nums) != q
987
+ if stop_run and not override:
988
+ logger.info(f"Stopping search at {q} classes")
989
+ break # Exit the loop immediately
990
+
991
+ best_model_idx += 1
992
+ # }
993
+ else:
994
+ # {
995
+ all_solutions = sorted(solutions, key=lambda sol: sol.obj[0])
996
+ best_solution = all_solutions[0] # assume already sorted
997
+ if best_solution.obj[0] < prev or override:
998
+ # {
999
+ best_model_idx += 1
1000
+ prev = best_solution.obj[0]
1001
+ # }
1002
+ else: # {
1003
+ break # Exit the loop immediately
1004
+ # }
1005
+ # }
1006
+ # }
1007
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1008
+ self.log_solutions(all_solutions)
1009
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1010
+
1011
+ best_val, all_val, all_val_classes = self.post_process(all_solutions)
1012
+
1013
+ if self.generate_plots:
1014
+ self.plot_results_latent(all_solutions, best_val, all_val, all_val_classes)
1015
+
1016
+ return all_solutions
1017
+ # }
1018
+
1019
+
1020
+ ''' ---------------------------------------------------------- '''
1021
+ ''' Function '''
1022
+ ''' ---------------------------------------------------------- '''
1023
+ def plot_multi(self, solutions, all_val):
1024
+ # {
1025
+ crit = self.param.criterions[:self.nb_crit]
1026
+ fig, ax = plt.subplots()
1027
+
1028
+ # ~~~~~~~~~~~
1029
+ # LINE 1
1030
+ # ~~~~~~~~~~~
1031
+ scaled_val = np.log(all_val[1]) if crit[1] == 'MAE' else all_val[1]
1032
+ line_1 = ax.scatter(all_val[0], scaled_val, label="All solutions", marker='o')
1033
+
1034
+ # ~~~~~~~~~~~
1035
+ # LINE 2
1036
+ # ~~~~~~~~~~~
1037
+ init_solns = [sol for sol in solutions if abs(sol.obj[0]) < BOUND]
1038
+ init_0 = [sol[crit[0]] for sol in init_solns]
1039
+ init_1 = [sol[crit[1]] for sol in init_solns]
1040
+ line_2 = ax.scatter(init_0, init_1, label="Initial solutions", marker='x')
1041
+
1042
+ # ~~~~~~~~~~~~~~~~~~~~
1043
+ # PARETO CALCULATIONS
1044
+ # ~~~~~~~~~~~~~~~~~~~~
1045
+ fronts = self.get_fronts(solutions)
1046
+ pareto = self.get_pareto(fronts, solutions)
1047
+ self.pareto_front = [sol for sol in pareto if abs(sol.obj[0]) < BOUND] # Store filtered
1048
+ pareto_0 = np.array([sol.obj[0] for sol in pareto])
1049
+ pareto_1 = np.array([sol.obj[1] for sol in pareto])
1050
+ pareto_1 = np.log(pareto_1) if crit[1] == 'MAE' else pareto_1
1051
+
1052
+ # ~~~~~~~~~~~
1053
+ # LINE 3
1054
+ # ~~~~~~~~~~~
1055
+ line_3 = ax.scatter(pareto_0, pareto_1, label="Pareto Front", marker='o')
1056
+
1057
+ # ~~~~~~~~~~~
1058
+ # LINE 4
1059
+ # ~~~~~~~~~~~
1060
+ # Determine the indices that would sort the pareto_0 array in ascending order.
1061
+ # These indices represent the positions of elements in the original array.
1062
+ pareto_idx = np.argsort(pareto_0)
1063
+ line_4 = ax.plot(pareto_0[pareto_idx], pareto_1[pareto_idx], color="r", label="Pareto Front")
1064
+
1065
+ # ~~~~~~~~~~~
1066
+ # ALL LINES
1067
+ # ~~~~~~~~~~~
1068
+ all_lines = (line_1, line_2, line_4[0])
1069
+
1070
+ labels = [line.get_label() for line in all_lines]
1071
+ log_str = 'log' if crit[1] == 'MAE' else ''
1072
+ ax.set_xlabel(f"{crit[0]} - Training dataset")
1073
+ ax.set_ylabel(f"{crit[1]} - Testing dataset")
1074
+ lgd = ax.legend(all_lines, labels, loc='upper right', bbox_to_anchor=(0.5, -0.1))
1075
+ current_time = datetime.datetime.now().strftime("%d%m%Y-%H%M%S")
1076
+ latent_info = "_" + str(self.param.num_classes) + "_classes_" if (self.param.num_classes > 1) else "_"
1077
+ plot_filename = self.code_name + "_" + latent_info + current_time + "_MOOF.png"
1078
+ plt.savefig(plot_filename, bbox_extra_artists=(lgd,), bbox_inches='tight')
1079
+ # }
1080
+
1081
+ def plot_multi_latent(self, solutions, all_val_classes):
1082
+ #
1083
+ crit = self.param.criterions[:self.nb_crit]
1084
+
1085
+ fronts = self.get_fronts(solutions)
1086
+ pareto = self.get_pareto(fronts, solutions)
1087
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1088
+ fig, ax = plt.subplots()
1089
+ lns_all = []
1090
+ for q in range(self.min_classes,self.max_classes):
1091
+ # {
1092
+ class_label = "All solutions - " + str(q) + " classes"
1093
+ if q == 1:
1094
+ class_label = "All solutions - " + str(q) + " class"
1095
+ lns = ax.scatter(all_val_classes[0][q], all_val_classes[1][q], label=class_label, marker='o')
1096
+ lns_all.append(lns)
1097
+ # }
1098
+ # ~~~~~~~~~~~
1099
+ # LINE 2
1100
+ # ~~~~~~~~~~~
1101
+ init_val = [[] for _ in range(self.nb_crit)]
1102
+ init_sols = [sol for sol in solutions if sol.obj[0] < BOUND and sol['is_initial_sol']]
1103
+ init_val[0] = [sol.obj[0] for sol in init_sols]
1104
+ if crit[0] == 'MAE': init_val[1] = np.log(init_val[1])
1105
+ init_val[1] = [sol.obj[1] for sol in init_sols]
1106
+ if crit[1] == 'MAE': init_val[1] = np.log(init_val[1])
1107
+ lns2 = ax.scatter(init_val[0], init_val[1], label="Initial solutions", marker='x', color='black')
1108
+
1109
+ # ~~~~~~~~~~~
1110
+ # LINE 4
1111
+ # ~~~~~~~~~~~
1112
+ pareto = [pareto for _, pareto in enumerate(pareto) if np.abs(pareto.obj[0]) < BOUND]
1113
+ logger.info('Final Pareto: {}'.format(str(pareto)))
1114
+ pareto_0 = np.array([par.obj[0] for par in pareto])
1115
+ pareto_1 = np.array([par.obj[1] for par in pareto])
1116
+ log_str = ''
1117
+ if crit[1] == 'MAE':
1118
+ pareto_1 = np.log(pareto_1)
1119
+ log_str = 'log'
1120
+
1121
+ pareto_idx = np.argsort(pareto_0)
1122
+ lns4 = ax.plot(pareto_0[pareto_idx], pareto_1[pareto_idx], color="r", label="Pareto Front")
1123
+
1124
+ # ~~~~~~~~~~~
1125
+ # ALL LINES
1126
+ # ~~~~~~~~~~~
1127
+ lns = (*lns_all, lns2, lns4[0])
1128
+
1129
+ labs = [l_pot.get_label() for l_pot in lns]
1130
+ ax.set_xlabel(f"{crit[0]} - Training dataset")
1131
+ ax.set_ylabel(f"{log_str} {crit[1]} - Testing dataset")
1132
+ lgd = ax.legend(lns, labs, loc='upper center', bbox_to_anchor=(0.5, -0.1))
1133
+ current_time = datetime.datetime.now().strftime("%d%m%Y-%H%M%S")
1134
+ plot_filename = self.code_name + "_" + current_time + "_MOOF.png"
1135
+ plt.savefig(plot_filename, bbox_extra_artists=(lgd,), bbox_inches='tight')
1136
+ # }
1137
+
1138
+ ''' ---------------------------------------------------------- '''
1139
+ ''' Function '''
1140
+ ''' ---------------------------------------------------------- '''
1141
+ def plot_single(self, all_val, best_val):
1142
+ # {
1143
+ fig, ax1 = plt.subplots()
1144
+ ax2 = ax1.twinx()
1145
+ ax1.xaxis.get_major_locator().set_params(integer=True)
1146
+
1147
+ crit = self.param.criterions[:self.nb_crit]
1148
+ label ="Solution estimated at current iteration (" + crit + ")"
1149
+ line_1 = ax1.plot(np.arange(len(all_val[0])), all_val[0], label=label)
1150
+
1151
+ label = "Best solution in memory at current iteration (" + crit + ")"
1152
+ line_2 = ax1.plot(np.arange(len(best_val[0])), best_val[0], label=label, linestyle="dotted")
1153
+
1154
+ label = "In-sample LL of best solution in memory at current iteration"
1155
+ line_3 = ax2.plot(np.arange(len(best_val[1])), best_val[1], label=label, linestyle="dashed")
1156
+
1157
+ all_lines = line_1 + line_2 + line_3
1158
+
1159
+ labels = [line.get_label() for line in all_lines]
1160
+ handles, _ = ax1.get_legend_handles_labels()
1161
+ lgd = ax1.legend(all_lines, labels, loc='upper center', bbox_to_anchor=(0.5, -0.1))
1162
+ ax1.set_xlabel("Iterations")
1163
+ ax1.set_ylabel(crit[0])
1164
+ ax2.set_ylabel(crit[1])
1165
+ current_time = datetime.datetime.now().strftime("%d%m%Y-%H%M%S")
1166
+ latent_info = "_" + str(self.param.num_classes) + "_classes_" if (self.param.num_classes > 1) else "_"
1167
+ plot_filename = self.code_name + "_" + latent_info + current_time + "_SOOF.png"
1168
+ plt.savefig(plot_filename, bbox_extra_artists=(lgd,), bbox_inches='tight')
1169
+ # }
1170
+
1171
+ def plot_single_latent(self, best_val, all_val, all_val_classes):
1172
+ # {
1173
+ fig, ax1 = plt.subplots()
1174
+ ax2 = ax1.twinx()
1175
+ ax1.xaxis.get_major_locator().set_params(integer=True)
1176
+
1177
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1178
+ counter = 0
1179
+ max_0 = np.max(all_val[0])
1180
+ for q in range(self.min_classes, self.max_classes):
1181
+ # {
1182
+ num_sols_in_class = len(all_val_classes[0][q])
1183
+ ax1.axvline(x=counter, color='r', linestyle='--')
1184
+ if q == 1: line_text = '1 class'
1185
+ else: line_text = str(q) + ' classes'
1186
+ ax1.text(counter, max_0, line_text)
1187
+ counter += num_sols_in_class
1188
+ # }
1189
+ all_val[0] = np.concatenate(np.array(all_val_classes[0]))
1190
+
1191
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1192
+ crit = self.param.criterions[:self.nb_crit]
1193
+ label ="Solution estimated at current iteration (" + crit + ")"
1194
+ line_1 = ax1.plot(np.arange(len(all_val[0])), all_val[0], label=label)
1195
+
1196
+ label = "Best solution in memory at current iteration (" + crit + ")"
1197
+ line_2 = ax1.plot(np.arange(len(best_val[0])), best_val[0], label=label, linestyle="dotted")
1198
+
1199
+ label = "In-sample LL of best solution in memory at current iteration"
1200
+ line_3 = ax2.plot(np.arange(len(best_val[1])), best_val[1], label=label, linestyle="dashed")
1201
+
1202
+ all_lines = line_1 + line_2 + line_3
1203
+
1204
+ labels = [line.get_label() for line in all_lines]
1205
+ handles, _ = ax1.get_legend_handles_labels()
1206
+ lgd = ax1.legend(all_lines, labels, loc='upper center', bbox_to_anchor=(0.5, -0.1))
1207
+ ax1.set_xlabel("Iterations")
1208
+ ax1.set_ylabel(crit[0])
1209
+ ax2.set_ylabel(crit[1])
1210
+ current_time = datetime.datetime.now().strftime("%d%m%Y-%H%M%S")
1211
+ latent_info = "_" + str(self.param.num_classes) + "_classes_" if (self.param.num_classes > 1) else "_"
1212
+ plot_filename = self.code_name + "_" + latent_info + current_time + "_SOOF.png"
1213
+ plt.savefig(plot_filename, bbox_extra_artists=(lgd,), bbox_inches='tight')
1214
+ # }
1215
+
1216
+ ''' ---------------------------------------------------------- '''
1217
+ ''' Function '''
1218
+ ''' ---------------------------------------------------------- '''
1219
+ def post_process(self, solutions):
1220
+ # {
1221
+ valid_solutions = [sol for sol in solutions if sol.obj[0] < BOUND]
1222
+ valid_solutions = sorted(valid_solutions, key=lambda sol: sol['sol_num'])
1223
+
1224
+ all_val_classes, all_val = [[] for _ in range(self.nb_crit)]
1225
+ for q in range(self.min_classes, self.max_classes + 1):
1226
+ # {
1227
+ for i in range(self.nb_crit):
1228
+ # {
1229
+ all_val[i] = [sol.obj[i] for sol in valid_solutions if sol['class_num'] == q]
1230
+ crit = self.param.crit(i)
1231
+ if crit == 'MAE': all_val[i] = np.log(all_val[i])
1232
+ all_val_classes[i].append(all_val[i])
1233
+ # }
1234
+ # }
1235
+
1236
+ best_val = [[] for _ in range(self.nb_crit)]
1237
+ for i in range(self.nb_crit):
1238
+ best_val[i] = self.get_best_val(self.param.criterions, all_val_classes[i])
1239
+
1240
+ return best_val, all_val, all_val_classes
1241
+ # }
1242
+ ''' ---------------------------------------------------------- '''
1243
+ ''' Function. '''
1244
+ ''' ---------------------------------------------------------- '''
1245
+ def plot_results(self, solutions, best_val, all_val):
1246
+ # {
1247
+ if self.nb_crit == 1:
1248
+ self.plot_single(all_val[0], best_val)
1249
+ else:
1250
+ self.plot_multi(solutions, all_val)
1251
+ # }
1252
+
1253
+ def plot_results_latent(self, solutions, best_val, all_val, all_val_classes):
1254
+ # {
1255
+ if self.nb_crit == 1:
1256
+ self.plot_single_latent(best_val, all_val, all_val_classes)
1257
+ else:
1258
+ self.plot_multi_latent(solutions, all_val_classes)
1259
+ # }
1260
+ # }
1261
+