SearchLibrium 0.0.199__tar.gz → 0.0.201__tar.gz
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.
- {searchlibrium-0.0.199/src/SearchLibrium.egg-info → searchlibrium-0.0.201}/PKG-INFO +1 -1
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/pyproject.toml +1 -1
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/call_meta.py +125 -7
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/harmony.py +182 -113
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/hspbil.py +27 -1
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/sapbil.py +44 -13
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/search.py +116 -23
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/siman.py +1 -1
- searchlibrium-0.0.201/src/SearchLibrium/version.txt +1 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201/src/SearchLibrium.egg-info}/PKG-INFO +1 -1
- searchlibrium-0.0.199/src/SearchLibrium/version.txt +0 -1
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/LICENSE +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/MANIFEST.in +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/README.md +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/setup.cfg +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/BEHier.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/Halton.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/MixedLogit.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/MixedLogitGSE.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/Mode_Activity_Nested.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/RandomP.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/SEARCH_SM_MARIO.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/Two_Level_Nest.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/__init__.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/__main__.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/_choice_model.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/_device.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/banditsa.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/boxcox_functions.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/constraints_builder.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/data/Swissmetro_final.csv +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/data/TravelMode.csv +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/data/electricity.csv +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/latent_class.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/logistic_regression.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/main.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/main_debug.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/mdcev.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/misc.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/mixed_logit.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/mixed_nested.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/mixedrrm.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/multinomial_logit.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/multinomial_nested.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/multinomial_probit.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/ordered_logit.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/ordered_logit_mixed.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/predict.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/rrm.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/sample_data.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/selection_models.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/setup.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/sparseea_agds.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/test_lc_de.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/test_mario_searches.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/test_sapbil_vs_banditsa.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/threshold.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium/zero_inflated_ordered_probit.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium.egg-info/SOURCES.txt +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium.egg-info/dependency_links.txt +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium.egg-info/entry_points.txt +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium.egg-info/requires.txt +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/src/SearchLibrium.egg-info/top_level.txt +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/tests/test_per_alternative_random.py +0 -0
- {searchlibrium-0.0.199 → searchlibrium-0.0.201}/tests/test_zero_inflated_ordered_probit.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: SearchLibrium
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.201
|
|
4
4
|
Summary: Automated specification search for discrete choice models (logit, mixed logit, nested logit, RRM and more) using metaheuristic optimisation
|
|
5
5
|
Author: Mario Fernando Taco Morales, Alexander Paz, Prithvi Beeramole, Robert Burdett
|
|
6
6
|
Author-email: Zeke Ahern <z.ahern@qut.edu.au>
|
|
@@ -100,7 +100,7 @@ Documentation = "https://pypi.org/project/SearchLibrium/"
|
|
|
100
100
|
realpython = "SearchLibrium.__main__:main"
|
|
101
101
|
|
|
102
102
|
[tool.bumpver]
|
|
103
|
-
current_version = "0.0.
|
|
103
|
+
current_version = "0.0.201"
|
|
104
104
|
version_pattern = "MAJOR.MINOR.PATCH"
|
|
105
105
|
commit_message = "[skip ci] Bump version {old_version} -> {new_version}"
|
|
106
106
|
commit = true
|
|
@@ -95,6 +95,8 @@ def estimate_ctrl(parameters, algorithm='sa', thorough=False, deep=False):
|
|
|
95
95
|
tuple
|
|
96
96
|
SA : (tI, tF, max_temp_steps, max_iter)
|
|
97
97
|
HS : (max_mem, maxiter, max_harm, min_harm, max_pitch, min_pitch)
|
|
98
|
+
AGDS: (pop_size, maxiter, pc0, pm0)
|
|
99
|
+
TA : (max_threshold, max_steps, max_iter)
|
|
98
100
|
"""
|
|
99
101
|
ps = _problem_size(parameters)
|
|
100
102
|
c = ps['complexity']
|
|
@@ -132,13 +134,13 @@ def estimate_ctrl(parameters, algorithm='sa', thorough=False, deep=False):
|
|
|
132
134
|
elif algorithm == 'hs':
|
|
133
135
|
# Harmony memory size and improvisation iterations scale with complexity
|
|
134
136
|
if c < 50:
|
|
135
|
-
max_mem, maxiter = max(
|
|
137
|
+
max_mem, maxiter = max(20, 20 * scale), max(100, 100 * scale)
|
|
136
138
|
elif c < 200:
|
|
137
|
-
max_mem, maxiter = max(
|
|
139
|
+
max_mem, maxiter = max(25, 25 * scale), max(300, 300 * scale)
|
|
138
140
|
elif c < 600:
|
|
139
|
-
max_mem, maxiter = max(
|
|
141
|
+
max_mem, maxiter = max(30, 30 * scale), max(500, 500 * scale)
|
|
140
142
|
else:
|
|
141
|
-
max_mem, maxiter = max(
|
|
143
|
+
max_mem, maxiter = max(40, 40 * scale), max(800, 800 * scale)
|
|
142
144
|
|
|
143
145
|
# Harmony/pitch rates adapt: wider exploration band for bigger problems
|
|
144
146
|
if deep or thorough:
|
|
@@ -164,8 +166,34 @@ def estimate_ctrl(parameters, algorithm='sa', thorough=False, deep=False):
|
|
|
164
166
|
pc0 = 0.9
|
|
165
167
|
ctrl = (pop_size, maxiter, pc0, None)
|
|
166
168
|
|
|
169
|
+
elif algorithm == 'ta':
|
|
170
|
+
# Threshold Accepting: threshold decreases linearly from max_threshold to 0
|
|
171
|
+
# over max_steps, with max_iter evaluations per step
|
|
172
|
+
if c < 50:
|
|
173
|
+
max_threshold = max(500, 500 * scale)
|
|
174
|
+
max_steps = max(50, 50 * scale)
|
|
175
|
+
max_iter = max(30, 30 * scale)
|
|
176
|
+
elif c < 200:
|
|
177
|
+
max_threshold = max(1000, 1000 * scale)
|
|
178
|
+
max_steps = max(100, 100 * scale)
|
|
179
|
+
max_iter = max(50, 50 * scale)
|
|
180
|
+
elif c < 600:
|
|
181
|
+
max_threshold = max(2000, 2000 * scale)
|
|
182
|
+
max_steps = max(150, 150 * scale)
|
|
183
|
+
max_iter = max(80, 80 * scale)
|
|
184
|
+
else:
|
|
185
|
+
max_threshold = max(5000, 5000 * scale)
|
|
186
|
+
max_steps = max(200, 200 * scale)
|
|
187
|
+
max_iter = max(100, 100 * scale)
|
|
188
|
+
|
|
189
|
+
if deep:
|
|
190
|
+
max_threshold = max(max_threshold, 10000)
|
|
191
|
+
max_steps = max(max_steps, 300)
|
|
192
|
+
|
|
193
|
+
ctrl = (max_threshold, max_steps, max_iter)
|
|
194
|
+
|
|
167
195
|
else:
|
|
168
|
-
raise ValueError(f"Unknown algorithm '{algorithm}'. Use 'sa', 'hs' or '
|
|
196
|
+
raise ValueError(f"Unknown algorithm '{algorithm}'. Use 'sa', 'hs', 'agds', or 'ta'.")
|
|
169
197
|
|
|
170
198
|
return ctrl
|
|
171
199
|
|
|
@@ -180,6 +208,13 @@ def _describe_ctrl(ctrl, algorithm):
|
|
|
180
208
|
'number of cooling steps',
|
|
181
209
|
'evaluations per cooling step',
|
|
182
210
|
)
|
|
211
|
+
elif algorithm == 'ta':
|
|
212
|
+
names = ('max_threshold', 'max_steps', 'max_iter')
|
|
213
|
+
hints = (
|
|
214
|
+
'initial threshold — higher = more exploration',
|
|
215
|
+
'number of threshold reduction steps',
|
|
216
|
+
'evaluations per threshold step',
|
|
217
|
+
)
|
|
183
218
|
elif algorithm == 'agds':
|
|
184
219
|
names = ('pop_size', 'maxiter', 'pc0', 'pm0')
|
|
185
220
|
hints = (
|
|
@@ -326,6 +361,9 @@ def call_siman(parameters, init_sol=None, ctrl=None, thorough=False, deep=False,
|
|
|
326
361
|
Maximum search depth for production runs.
|
|
327
362
|
**kwargs
|
|
328
363
|
``id_num`` — run identifier (int, used in log file names).
|
|
364
|
+
``max_time`` — maximum wall-clock time in seconds (default: inf).
|
|
365
|
+
``max_total_iter`` — maximum total temperature steps (default: 100000).
|
|
366
|
+
``calibrate_tI`` — whether to auto-calibrate initial temperature (default: True if ctrl not provided).
|
|
329
367
|
Any other kwargs are forwarded to the SA constructor.
|
|
330
368
|
|
|
331
369
|
Returns
|
|
@@ -383,6 +421,11 @@ def call_sapbil(parameters, init_sol=None, ctrl=None, **kwargs):
|
|
|
383
421
|
If omitted the values are estimated from the problem size.
|
|
384
422
|
**kwargs
|
|
385
423
|
``id_num`` — run identifier (int, used in log file names).
|
|
424
|
+
``pbil_l_bounds`` — dict of learning rate bounds per decision type
|
|
425
|
+
(default: thesis-based values from Taco-Morales 2026).
|
|
426
|
+
Example: {"inclusion": (0.02, 0.25), "random": (0.02, 0.15), ...}
|
|
427
|
+
``pbil_p_low`` — minimum probability clamp (default: 0.05).
|
|
428
|
+
``pbil_p_high`` — maximum probability clamp (default: 0.95).
|
|
386
429
|
Any other kwargs are forwarded to the SAPBIL constructor.
|
|
387
430
|
|
|
388
431
|
Returns
|
|
@@ -430,6 +473,9 @@ def call_banditsa(parameters, init_sol=None, ctrl=None, **kwargs):
|
|
|
430
473
|
If omitted the values are estimated from the problem size.
|
|
431
474
|
**kwargs
|
|
432
475
|
``id_num`` - run identifier (int, used in log file names).
|
|
476
|
+
``bandit_prior_alpha`` — Thompson sampling prior alpha (default: 1.0).
|
|
477
|
+
``bandit_prior_beta`` — Thompson sampling prior beta (default: 1.0).
|
|
478
|
+
``bandit_epsilon`` — epsilon for epsilon-greedy exploration (default: 0.05).
|
|
433
479
|
Any other kwargs are forwarded to the BanditSA constructor.
|
|
434
480
|
|
|
435
481
|
Returns
|
|
@@ -485,6 +531,10 @@ def call_harmony(parameters, init_sol=None, ctrl=None, thorough=False, deep=Fals
|
|
|
485
531
|
Overrides thorough. Default False.
|
|
486
532
|
**kwargs
|
|
487
533
|
``id_num`` — run identifier.
|
|
534
|
+
``prop_local`` — proportion of iterations before local search (default: 0.8).
|
|
535
|
+
``threshold`` — convergence threshold (default: 15).
|
|
536
|
+
``generate_plots`` — whether to generate convergence plots (default: False).
|
|
537
|
+
Any other kwargs are forwarded to the HarmonySearch constructor via set_control_parameters.
|
|
488
538
|
|
|
489
539
|
Returns
|
|
490
540
|
-------
|
|
@@ -509,6 +559,9 @@ def call_harmony(parameters, init_sol=None, ctrl=None, thorough=False, deep=Fals
|
|
|
509
559
|
print()
|
|
510
560
|
|
|
511
561
|
solver = HarmonySearch(parameters, ctrl=ctrl, idnum=id_num)
|
|
562
|
+
# Forward remaining kwargs to set_control_parameters
|
|
563
|
+
if kwargs:
|
|
564
|
+
solver.set_control_parameters(**kwargs)
|
|
512
565
|
existing = [init_sol] if init_sol is not None else None
|
|
513
566
|
solver.run_search(existing_sols=existing)
|
|
514
567
|
solver.close_files()
|
|
@@ -539,6 +592,9 @@ def call_agds(parameters, init_sol=None, ctrl=None, thorough=False, deep=False,
|
|
|
539
592
|
Scale up population / generations for a more thorough search.
|
|
540
593
|
**kwargs
|
|
541
594
|
``id_num`` — run identifier.
|
|
595
|
+
``ref_divisions`` — NSGA-III reference point divisions (default: 12).
|
|
596
|
+
``generate_plots`` — whether to generate convergence plots (default: False).
|
|
597
|
+
Any other kwargs are forwarded to the SparseEAAGDS constructor via set_control_parameters.
|
|
542
598
|
|
|
543
599
|
Returns
|
|
544
600
|
-------
|
|
@@ -563,6 +619,9 @@ def call_agds(parameters, init_sol=None, ctrl=None, thorough=False, deep=False,
|
|
|
563
619
|
print()
|
|
564
620
|
|
|
565
621
|
solver = SparseEAAGDS(parameters, ctrl=ctrl, idnum=id_num)
|
|
622
|
+
# Forward remaining kwargs to set_control_parameters
|
|
623
|
+
if kwargs:
|
|
624
|
+
solver.set_control_parameters(**kwargs)
|
|
566
625
|
existing = [init_sol] if init_sol is not None else None
|
|
567
626
|
solver.run_search(existing_sols=existing)
|
|
568
627
|
solver.close_files()
|
|
@@ -589,6 +648,14 @@ def call_harmony_pbil(parameters, init_sol=None, ctrl=None, **kwargs):
|
|
|
589
648
|
If omitted the values are estimated from the problem size.
|
|
590
649
|
**kwargs
|
|
591
650
|
``id_num`` — run identifier.
|
|
651
|
+
``pbil_l_bounds`` — dict of learning rate bounds per decision type
|
|
652
|
+
(default: thesis-based values from Taco-Morales 2026).
|
|
653
|
+
``pbil_p_low`` — minimum probability clamp (default: 0.05).
|
|
654
|
+
``pbil_p_high`` — maximum probability clamp (default: 0.95).
|
|
655
|
+
``prop_local`` — proportion of iterations before local search (default: 0.8).
|
|
656
|
+
``threshold`` — convergence threshold (default: 15).
|
|
657
|
+
``generate_plots`` — whether to generate convergence plots (default: False).
|
|
658
|
+
Any other kwargs are forwarded to the HSPBIL constructor.
|
|
592
659
|
|
|
593
660
|
Returns
|
|
594
661
|
-------
|
|
@@ -610,7 +677,7 @@ def call_harmony_pbil(parameters, init_sol=None, ctrl=None, **kwargs):
|
|
|
610
677
|
print(_describe_ctrl(ctrl, 'hs'))
|
|
611
678
|
print()
|
|
612
679
|
|
|
613
|
-
solver = HSPBIL(parameters, init_sol, ctrl, idnum=id_num)
|
|
680
|
+
solver = HSPBIL(parameters, init_sol, ctrl, idnum=id_num, **kwargs)
|
|
614
681
|
existing = [init_sol] if init_sol is not None else None
|
|
615
682
|
solver.run_search(existing_sols=existing)
|
|
616
683
|
solver.close_files()
|
|
@@ -659,6 +726,7 @@ def call_search(parameters, init_sol=None, algorithm='sa', ctrl=None, thorough=F
|
|
|
659
726
|
>>> best = call_search(params) # SA, auto ctrl
|
|
660
727
|
>>> best = call_search(params, algorithm='banditsa') # BanditSA, auto ctrl
|
|
661
728
|
>>> best = call_search(params, algorithm='hs') # HS, auto ctrl
|
|
729
|
+
>>> best = call_search(params, algorithm='ta') # TA, auto ctrl
|
|
662
730
|
>>> best = call_search(params, thorough=True) # Thorough SA
|
|
663
731
|
>>> best = call_search(params, algorithm='hs', deep=True) # Deep HS
|
|
664
732
|
>>> best = call_search(params, ctrl=(500,0.001,80,15))# SA, manual ctrl
|
|
@@ -678,13 +746,63 @@ def call_search(parameters, init_sol=None, algorithm='sa', ctrl=None, thorough=F
|
|
|
678
746
|
thorough=thorough, deep=deep, **kwargs)
|
|
679
747
|
elif algorithm in ('hspbil', 'harmony_pbil', 'hs_pbil', 'hs+pbil'):
|
|
680
748
|
return call_harmony_pbil(parameters, init_sol=init_sol, ctrl=ctrl, **kwargs)
|
|
749
|
+
elif algorithm in ('ta', 'threshold', 'threshold_accepting'):
|
|
750
|
+
return call_threshold(parameters, init_sol=init_sol, ctrl=ctrl, **kwargs)
|
|
681
751
|
else:
|
|
682
752
|
raise ValueError(
|
|
683
753
|
f"Unknown algorithm '{algorithm}'. "
|
|
684
|
-
f"Choose 'sa', 'sapbil', 'banditsa', 'hs', or '
|
|
754
|
+
f"Choose 'sa', 'sapbil', 'banditsa', 'hs', 'hspbil', or 'ta'."
|
|
685
755
|
)
|
|
686
756
|
|
|
687
757
|
|
|
758
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
759
|
+
# Threshold Accepting
|
|
760
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
761
|
+
|
|
762
|
+
def call_threshold(parameters, init_sol=None, ctrl=None, **kwargs):
|
|
763
|
+
"""
|
|
764
|
+
Run Threshold Accepting search.
|
|
765
|
+
|
|
766
|
+
Parameters
|
|
767
|
+
----------
|
|
768
|
+
parameters : Parameters
|
|
769
|
+
Problem definition.
|
|
770
|
+
init_sol : Solution, optional
|
|
771
|
+
Warm-start solution.
|
|
772
|
+
ctrl : tuple, optional
|
|
773
|
+
``(max_threshold, max_steps, max_iter)``. If omitted the values are estimated
|
|
774
|
+
from the problem size.
|
|
775
|
+
**kwargs
|
|
776
|
+
``id_num`` — run identifier.
|
|
777
|
+
|
|
778
|
+
Returns
|
|
779
|
+
-------
|
|
780
|
+
Solution
|
|
781
|
+
Best solution found.
|
|
782
|
+
"""
|
|
783
|
+
if ctrl is None:
|
|
784
|
+
ctrl = kwargs.pop('ctrl', None)
|
|
785
|
+
|
|
786
|
+
id_num = kwargs.pop('id_num', None)
|
|
787
|
+
|
|
788
|
+
if ctrl is None:
|
|
789
|
+
ctrl = estimate_ctrl(parameters, algorithm='ta')
|
|
790
|
+
print(f"[TA] Auto-estimated hyperparameters (problem complexity "
|
|
791
|
+
f"= {_problem_size(parameters)['complexity']}):")
|
|
792
|
+
else:
|
|
793
|
+
print("[TA] Using provided hyperparameters:")
|
|
794
|
+
|
|
795
|
+
print(_describe_ctrl(ctrl, 'ta'))
|
|
796
|
+
print()
|
|
797
|
+
|
|
798
|
+
solver = TA(parameters, init_sol, ctrl, id_num, **kwargs)
|
|
799
|
+
solver.run()
|
|
800
|
+
solver.close_files()
|
|
801
|
+
best = solver.return_best()
|
|
802
|
+
_print_dashboard(solver, best, algorithm='TA')
|
|
803
|
+
return best
|
|
804
|
+
|
|
805
|
+
|
|
688
806
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
689
807
|
# Parallel SA variants (unchanged interface, improved ctrl handling)
|
|
690
808
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -255,106 +255,96 @@ class HarmonySearch(Search):
|
|
|
255
255
|
''' A new solution, could either be built from an existing one '''
|
|
256
256
|
''' or constructed randomly. '''
|
|
257
257
|
''' ---------------------------------------------------------- '''
|
|
258
|
-
def build_solution(self, memory,
|
|
258
|
+
def build_solution(self, memory, harm_rate):
|
|
259
259
|
# {
|
|
260
|
-
"""
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
#
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
#
|
|
316
|
-
new_randvars = {k: v for k, v in
|
|
260
|
+
""" Build a new solution using Harmony Memory Consideration.
|
|
261
|
+
|
|
262
|
+
For each candidate variable, with probability harm_rate (HMCR), copy
|
|
263
|
+
the value from a randomly selected harmony in memory; otherwise
|
|
264
|
+
generate a random value. This is the standard HS approach.
|
|
265
|
+
"""
|
|
266
|
+
new_sol = Solution(nb_crit=self.nb_crit)
|
|
267
|
+
|
|
268
|
+
# Collect all candidate variables
|
|
269
|
+
all_as_candidates = list(self.param.asvarnames or [])
|
|
270
|
+
all_is_candidates = list(self.param.isvarnames or [])
|
|
271
|
+
|
|
272
|
+
# --- AS variables: for each, decide via HMCR ---
|
|
273
|
+
new_asvars = []
|
|
274
|
+
for var in all_as_candidates:
|
|
275
|
+
if self.param.generator.rand() <= harm_rate:
|
|
276
|
+
# Copy from memory: randomly pick a harmony and take its value for this var
|
|
277
|
+
if memory:
|
|
278
|
+
chosen = self.param.generator.choice(len(memory))
|
|
279
|
+
if var in memory[chosen].get('asvars', []):
|
|
280
|
+
new_asvars.append(var)
|
|
281
|
+
else:
|
|
282
|
+
# Random generation: include with some base probability
|
|
283
|
+
if self.param.generator.rand() < 0.3: # base inclusion probability
|
|
284
|
+
new_asvars.append(var)
|
|
285
|
+
|
|
286
|
+
# Enforce pre-specified
|
|
287
|
+
new_asvars = sorted(list(set().union(new_asvars, self.param.ps_asvars or [])))
|
|
288
|
+
new_asvars = self.remove_redundant_asvars(new_asvars, self.param.trans_asvars, self.param.asvarnames)
|
|
289
|
+
new_asvars = self.remove_collinear_vars(new_asvars)
|
|
290
|
+
new_asvars = self._apply_mutual_exclusion_filter(new_asvars)
|
|
291
|
+
new_sol['asvars'] = new_asvars
|
|
292
|
+
|
|
293
|
+
# --- IS variables: same HMCR logic ---
|
|
294
|
+
new_isvars = []
|
|
295
|
+
for var in all_is_candidates:
|
|
296
|
+
if self.param.generator.rand() <= harm_rate:
|
|
297
|
+
if memory:
|
|
298
|
+
chosen = self.param.generator.choice(len(memory))
|
|
299
|
+
if var in memory[chosen].get('isvars', []):
|
|
300
|
+
new_isvars.append(var)
|
|
301
|
+
else:
|
|
302
|
+
if self.param.generator.rand() < 0.3:
|
|
303
|
+
new_isvars.append(var)
|
|
304
|
+
|
|
305
|
+
new_isvars = sorted(list(set().union(new_isvars, self.param.ps_isvars or [])))
|
|
306
|
+
new_isvars = self.remove_collinear_vars(new_isvars)
|
|
307
|
+
new_isvars = self._apply_mutual_exclusion_filter(new_isvars)
|
|
308
|
+
new_sol['isvars'] = new_isvars
|
|
309
|
+
|
|
310
|
+
# --- Derived features (randvars, bcvars, corvars) from chosen harmony ---
|
|
311
|
+
# Pick one reference harmony for derived features
|
|
312
|
+
if memory and (new_asvars or new_isvars):
|
|
313
|
+
ref_sol = memory[self.param.generator.choice(len(memory))]
|
|
314
|
+
|
|
315
|
+
# Random coefficients: only for variables that are in asvars
|
|
316
|
+
new_randvars = {k: v for k, v in ref_sol.get('randvars', {}).items() if k in new_asvars}
|
|
317
317
|
new_sol['randvars'] = new_randvars
|
|
318
318
|
|
|
319
|
-
|
|
320
|
-
|
|
319
|
+
# Box-Cox: only for vars in asvars, not in corvars
|
|
320
|
+
ref_bcvars = ref_sol.get('bcvars', [])
|
|
321
|
+
new_bcvars = [v for v in ref_bcvars if v in new_asvars and v not in self.param.ps_corvars]
|
|
321
322
|
new_sol['bcvars'] = new_bcvars
|
|
322
323
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
324
|
+
# Correlation: only for randvars, not bcvars
|
|
325
|
+
ref_corvars = ref_sol.get('corvars', [])
|
|
326
|
+
if ref_corvars:
|
|
327
|
+
new_corvars = [v for v in ref_corvars if v in new_randvars and v not in new_bcvars]
|
|
328
|
+
new_sol['corvars'] = new_corvars
|
|
329
|
+
else:
|
|
330
|
+
new_sol['corvars'] = []
|
|
328
331
|
|
|
329
|
-
#
|
|
330
|
-
new_sol['asc_ind'] =
|
|
332
|
+
# Intercept
|
|
333
|
+
new_sol['asc_ind'] = ref_sol.get('asc_ind', False)
|
|
331
334
|
|
|
332
|
-
#
|
|
335
|
+
# Latent class specs
|
|
333
336
|
if getattr(self.param, 'allow_latent_corvars', False):
|
|
334
|
-
if
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
346
|
-
if chosen_sol['member_params_spec'] is not None:
|
|
347
|
-
# {
|
|
348
|
-
member_params_spec = copy.deepcopy(chosen_sol['member_params_spec'])
|
|
349
|
-
for ii, member_params in enumerate(member_params_spec):
|
|
350
|
-
# {
|
|
351
|
-
member_params_index = self.param.generator.choice(bin, size=len(member_params), p=prob)
|
|
352
|
-
member_params_spec[ii] = np.array([i for (i, v) in zip(member_params, member_params_index) if v],
|
|
353
|
-
dtype=member_params.dtype)
|
|
354
|
-
# }
|
|
355
|
-
new_sol['member_params_spec'] = member_params_spec
|
|
356
|
-
# }
|
|
357
|
-
# }
|
|
337
|
+
if ref_sol.get('class_params_spec') is not None:
|
|
338
|
+
new_sol['class_params_spec'] = copy.deepcopy(ref_sol['class_params_spec'])
|
|
339
|
+
if ref_sol.get('member_params_spec') is not None:
|
|
340
|
+
new_sol['member_params_spec'] = copy.deepcopy(ref_sol['member_params_spec'])
|
|
341
|
+
else:
|
|
342
|
+
new_sol['randvars'] = {}
|
|
343
|
+
new_sol['bcvars'] = []
|
|
344
|
+
new_sol['corvars'] = []
|
|
345
|
+
new_sol['asc_ind'] = False
|
|
346
|
+
new_sol['class_params_spec'] = None
|
|
347
|
+
new_sol['member_params_spec'] = None
|
|
358
348
|
|
|
359
349
|
self._enforce_mutual_exclusion(new_sol)
|
|
360
350
|
return new_sol
|
|
@@ -425,33 +415,79 @@ class HarmonySearch(Search):
|
|
|
425
415
|
|
|
426
416
|
''' ---------------------------------------------------------- '''
|
|
427
417
|
''' Function. Performs the pitch adjustment operation to '''
|
|
428
|
-
''' fine-tune a given solution.
|
|
429
|
-
'''
|
|
430
|
-
'''
|
|
431
|
-
'''
|
|
418
|
+
''' fine-tune a given solution. In classic HS, this is a '''
|
|
419
|
+
''' small perturbation (x_new = x_old +- bw * rand()). For '''
|
|
420
|
+
''' discrete model specification, we make SMALL changes: '''
|
|
421
|
+
''' toggle one variable, change one distribution, etc. '''
|
|
432
422
|
''' ---------------------------------------------------------- '''
|
|
433
423
|
def pitch_adjustment(self, sol, pitch):
|
|
434
424
|
# {
|
|
435
425
|
adjusted_solution = copy.deepcopy(sol)
|
|
436
|
-
perturbations = [self.perturb_asfeature, self.perturb_isfeature, self.perturb_model_t]
|
|
437
|
-
|
|
438
|
-
if self.param.allow_random:
|
|
439
|
-
perturbations.append(self.perturb_randfeature)
|
|
440
|
-
if adjusted_solution['randvars']:
|
|
441
|
-
perturbations.append(self.perturb_distribution)
|
|
442
426
|
|
|
427
|
+
# Bandwidth controls how many small changes to make
|
|
428
|
+
# In standard HS: x_new = x_old + bw * randn()
|
|
429
|
+
# Here: bw ~ pitch, make 0-2 small changes
|
|
430
|
+
n_changes = 0
|
|
431
|
+
if self.param.generator.rand() < pitch:
|
|
432
|
+
n_changes = 1
|
|
433
|
+
if self.param.generator.rand() < pitch:
|
|
434
|
+
n_changes = 2
|
|
435
|
+
|
|
436
|
+
# Build list of SMALL adjustment operations
|
|
437
|
+
small_adjustments = []
|
|
438
|
+
|
|
439
|
+
# AS variable: add one missing or remove one present (small toggle)
|
|
440
|
+
if self.param.asvarnames:
|
|
441
|
+
missing_as = [v for v in self.param.asvarnames if v not in adjusted_solution.get('asvars', [])]
|
|
442
|
+
present_as = [v for v in adjusted_solution.get('asvars', []) if v not in self.param.ps_asvars]
|
|
443
|
+
if missing_as:
|
|
444
|
+
small_adjustments.append(('add_as', lambda s: self._add_one_asvar(s, missing_as)))
|
|
445
|
+
if present_as:
|
|
446
|
+
small_adjustments.append(('rem_as', lambda s: self._remove_one_asvar(s, present_as)))
|
|
447
|
+
|
|
448
|
+
# IS variable: similar
|
|
449
|
+
if self.param.isvarnames:
|
|
450
|
+
missing_is = [v for v in self.param.isvarnames if v not in adjusted_solution.get('isvars', [])]
|
|
451
|
+
present_is = [v for v in adjusted_solution.get('isvars', []) if v not in self.param.ps_isvars]
|
|
452
|
+
if missing_is:
|
|
453
|
+
small_adjustments.append(('add_is', lambda s: self._add_one_isvar(s, missing_is)))
|
|
454
|
+
if present_is:
|
|
455
|
+
small_adjustments.append(('rem_is', lambda s: self._remove_one_isvar(s, present_is)))
|
|
456
|
+
|
|
457
|
+
# Random coefficient: change distribution of ONE random var
|
|
458
|
+
if self.param.allow_random and adjusted_solution.get('randvars'):
|
|
459
|
+
small_adjustments.append(('chg_dist', self.perturb_distribution))
|
|
460
|
+
|
|
461
|
+
# Box-Cox: add/remove one
|
|
443
462
|
if self.param.allow_bcvars:
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
463
|
+
missing_bc = [v for v in adjusted_solution.get('asvars', [])
|
|
464
|
+
if v not in adjusted_solution.get('bcvars', []) and v not in self.param.ps_corvars]
|
|
465
|
+
present_bc = [v for v in adjusted_solution.get('bcvars', []) if v not in self.param.ps_bcvars]
|
|
466
|
+
if missing_bc:
|
|
467
|
+
small_adjustments.append(('add_bc', lambda s: self._add_one_bcvar(s, missing_bc)))
|
|
468
|
+
if present_bc:
|
|
469
|
+
small_adjustments.append(('rem_bc', lambda s: self._remove_one_bcvar(s, present_bc)))
|
|
470
|
+
|
|
471
|
+
# Correlation: add/remove one (needs >=2 randvars)
|
|
472
|
+
if self.param.allow_corvars and len(adjusted_solution.get('randvars', {})) >= 2:
|
|
473
|
+
missing_cor = [v for v in adjusted_solution.get('randvars', {})
|
|
474
|
+
if v not in adjusted_solution.get('corvars', []) and v not in adjusted_solution.get('bcvars', [])]
|
|
475
|
+
present_cor = [v for v in adjusted_solution.get('corvars', []) if v not in self.param.ps_corvars]
|
|
476
|
+
if missing_cor:
|
|
477
|
+
small_adjustments.append(('add_cor', lambda s: self._add_one_corvar(s, missing_cor)))
|
|
478
|
+
if present_cor:
|
|
479
|
+
small_adjustments.append(('rem_cor', lambda s: self._remove_one_corvar(s, present_cor)))
|
|
480
|
+
|
|
481
|
+
# Model type change (rare)
|
|
482
|
+
if self.param.avail_models is not None and len(self.param.avail_models) > 1:
|
|
483
|
+
small_adjustments.append(('chg_model', self.perturb_model_t))
|
|
484
|
+
|
|
485
|
+
# Apply n_changes small adjustments
|
|
486
|
+
for _ in range(n_changes):
|
|
487
|
+
if not small_adjustments:
|
|
488
|
+
break
|
|
489
|
+
_, adj_fn = self.param.generator.choice(small_adjustments)
|
|
490
|
+
adjusted_solution = adj_fn(adjusted_solution)
|
|
455
491
|
|
|
456
492
|
adjusted_solution['randvars'] = self.normalize_randvars(
|
|
457
493
|
adjusted_solution['asvars'],
|
|
@@ -463,6 +499,39 @@ class HarmonySearch(Search):
|
|
|
463
499
|
return adjusted_solution, converged
|
|
464
500
|
# }
|
|
465
501
|
|
|
502
|
+
# --- Helper methods for small single-variable adjustments ---
|
|
503
|
+
def _add_one_asvar(self, sol, candidates):
|
|
504
|
+
var = self.param.generator.choice(candidates)
|
|
505
|
+
return self.perturb_add_asfeature(sol) if hasattr(self, 'perturb_add_asfeature') else sol
|
|
506
|
+
|
|
507
|
+
def _remove_one_asvar(self, sol, candidates):
|
|
508
|
+
var = self.param.generator.choice(candidates)
|
|
509
|
+
return self.perturb_remove_asfeature(sol) if hasattr(self, 'perturb_remove_asfeature') else sol
|
|
510
|
+
|
|
511
|
+
def _add_one_isvar(self, sol, candidates):
|
|
512
|
+
var = self.param.generator.choice(candidates)
|
|
513
|
+
return self.perturb_add_isfeature(sol) if hasattr(self, 'perturb_add_isfeature') else sol
|
|
514
|
+
|
|
515
|
+
def _remove_one_isvar(self, sol, candidates):
|
|
516
|
+
var = self.param.generator.choice(candidates)
|
|
517
|
+
return self.perturb_remove_isfeature(sol) if hasattr(self, 'perturb_remove_isfeature') else sol
|
|
518
|
+
|
|
519
|
+
def _add_one_bcvar(self, sol, candidates):
|
|
520
|
+
var = self.param.generator.choice(candidates)
|
|
521
|
+
return self.perturb_add_bcfeature(sol) if hasattr(self, 'perturb_add_bcfeature') else sol
|
|
522
|
+
|
|
523
|
+
def _remove_one_bcvar(self, sol, candidates):
|
|
524
|
+
var = self.param.generator.choice(candidates)
|
|
525
|
+
return self.perturb_remove_bcfeature(sol) if hasattr(self, 'perturb_remove_bcfeature') else sol
|
|
526
|
+
|
|
527
|
+
def _add_one_corvar(self, sol, candidates):
|
|
528
|
+
var = self.param.generator.choice(candidates)
|
|
529
|
+
return self.perturb_add_corfeature(sol) if hasattr(self, 'perturb_add_corfeature') else sol
|
|
530
|
+
|
|
531
|
+
def _remove_one_corvar(self, sol, candidates):
|
|
532
|
+
var = self.param.generator.choice(candidates)
|
|
533
|
+
return self.perturb_remove_corfeature(sol) if hasattr(self, 'perturb_remove_corfeature') else sol
|
|
534
|
+
|
|
466
535
|
''' ---------------------------------------------------------- '''
|
|
467
536
|
''' Function. Extracts the best features '''
|
|
468
537
|
''' ---------------------------------------------------------- '''
|
|
@@ -34,14 +34,40 @@ class HSPBIL(HarmonySearch):
|
|
|
34
34
|
|
|
35
35
|
Overrides the standard uniform-random pitch perturbations with
|
|
36
36
|
probability-weighted decisions that learn from accepted solutions.
|
|
37
|
+
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
param : Parameters
|
|
41
|
+
Problem definition (from search.py).
|
|
42
|
+
init_sol : Solution or None
|
|
43
|
+
Optional warm-start solution.
|
|
44
|
+
ctrl : tuple
|
|
45
|
+
``(max_mem, maxiter, max_harm, min_harm, max_pitch, min_pitch)``
|
|
46
|
+
idnum : int or str
|
|
47
|
+
Run identifier for log files.
|
|
48
|
+
**kwargs
|
|
49
|
+
Forwarded to the :class:`HarmonySearch` base class constructor.
|
|
50
|
+
PBIL-specific kwargs:
|
|
51
|
+
``pbil_l_bounds`` — dict of learning rate bounds per decision type
|
|
52
|
+
(default: thesis-based values from Taco-Morales 2026).
|
|
53
|
+
``pbil_p_low`` — minimum probability clamp (default: 0.05).
|
|
54
|
+
``pbil_p_high`` — maximum probability clamp (default: 0.95).
|
|
37
55
|
"""
|
|
38
56
|
|
|
39
57
|
def __init__(self, param, init_sol, ctrl, idnum=0, **kwargs):
|
|
58
|
+
# Extract PBIL-specific kwargs before passing to parent
|
|
59
|
+
pbil_l_bounds = kwargs.pop('pbil_l_bounds', None)
|
|
60
|
+
pbil_p_low = kwargs.pop('pbil_p_low', None)
|
|
61
|
+
pbil_p_high = kwargs.pop('pbil_p_high', None)
|
|
62
|
+
|
|
40
63
|
super().__init__(param, ctrl=ctrl, idnum=idnum)
|
|
41
64
|
|
|
42
65
|
varnames = list(param.asvarnames or [])
|
|
43
66
|
distributions = list(param.distr or ["n", "ln", "tn", "u", "t"])
|
|
44
|
-
self.prob_matrix = ProbabilityMatrix(varnames, distributions
|
|
67
|
+
self.prob_matrix = ProbabilityMatrix(varnames, distributions,
|
|
68
|
+
l_bounds=pbil_l_bounds,
|
|
69
|
+
p_low=pbil_p_low,
|
|
70
|
+
p_high=pbil_p_high)
|
|
45
71
|
|
|
46
72
|
self._ps_asvars = set(getattr(param, "ps_asvars", []) or [])
|
|
47
73
|
self._ps_randvars = set(
|