SearchLibrium 0.0.127__tar.gz → 0.0.129__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.127 → searchlibrium-0.0.129}/PKG-INFO +141 -23
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/README.md +137 -22
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/pyproject.toml +4 -1
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/__init__.py +2 -2
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/harmony.py +331 -10
- searchlibrium-0.0.129/src/SearchLibrium/version.txt +1 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium.egg-info/PKG-INFO +141 -23
- searchlibrium-0.0.127/src/SearchLibrium/version.txt +0 -1
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/setup.cfg +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/Halton.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/MixedLogit.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/Mode_Activity_Nested.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/RandomP.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/SEARCH_SM_MARIO.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/Two_Level_Nest.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/__main__.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/_choice_model.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/_device.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/banditsa.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/bhhh/minimize.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/boxcox_functions.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/call_meta.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/constraints_builder.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/hspbil.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/latent_class.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/main.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/main_debug.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/mdcev.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/misc.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/mixed_logit.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/mixed_nested.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/mixedrrm.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/multinomial_logit.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/multinomial_nested.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/multinomial_probit.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/ordered_logit.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/ordered_logit_mixed.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/predict.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/rrm.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/sapbil.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/search.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/selection_models.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/setup.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/siman.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/test_lc_de.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/test_mario_searches.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/test_sapbil_vs_banditsa.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/threshold.py +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium.egg-info/SOURCES.txt +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium.egg-info/dependency_links.txt +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium.egg-info/entry_points.txt +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium.egg-info/requires.txt +0 -0
- {searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium.egg-info/top_level.txt +0 -0
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: SearchLibrium
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.129
|
|
4
4
|
Summary: A Python package for econometric models driven by search
|
|
5
5
|
Author: Mario Fernando Taco Morales, Alexander Paz, Prithvi Beeramole, Robert Burdett
|
|
6
6
|
Author-email: Zeke Ahern <z.ahern@qut.edu.au>
|
|
7
7
|
Project-URL: Homepage, https://pypi.org/project/SearchLibrium/
|
|
8
|
+
Project-URL: Repository, https://github.com/zahern/HypothesisX
|
|
9
|
+
Project-URL: Issues, https://github.com/zahern/HypothesisX/issues
|
|
10
|
+
Project-URL: Documentation, https://pypi.org/project/SearchLibrium/
|
|
8
11
|
Keywords: econometric models,search,discrete choice,logit,probit
|
|
9
12
|
Classifier: Development Status :: 4 - Beta
|
|
10
13
|
Classifier: Intended Audience :: Science/Research
|
|
@@ -41,8 +44,9 @@ Requires-Dist: pytest; extra == "dev"
|
|
|
41
44
|
[](https://pypi.org/project/SearchLibrium/)
|
|
42
45
|
[](LICENSE)
|
|
43
46
|
[](https://github.com/zahern/HypothesisX/actions/workflows/ci.yml)
|
|
47
|
+
[](https://github.com/zahern/HypothesisX)
|
|
44
48
|
|
|
45
|
-
**Automated discrete choice model search powered by Simulated Annealing, Harmony Search, and JAX-accelerated MLE.**
|
|
49
|
+
**Automated discrete choice model search powered by Simulated Annealing, Bandit-guided SA, Harmony Search, SA+PBIL, HS+PBIL, Parallel SA, and JAX-accelerated MLE.**
|
|
46
50
|
|
|
47
51
|
SearchLibrium searches over model specifications — which variables to include, whether parameters should be random, which transformations to apply, and which model class to use — and returns the best converged, all-significant model according to your chosen criterion (BIC, AIC, log-likelihood, MAE, or multi-objective combinations).
|
|
48
52
|
|
|
@@ -122,16 +126,19 @@ A **run dashboard** is printed automatically at the end of every search, showing
|
|
|
122
126
|
|
|
123
127
|
## How the search works
|
|
124
128
|
|
|
125
|
-
The search uses **
|
|
129
|
+
The search uses **metaheuristic optimisation** (SA, BanditSA, Harmony Search,
|
|
130
|
+
SA+PBIL, HS+PBIL, or parallel variants) to explore the space of model
|
|
131
|
+
specifications:
|
|
126
132
|
|
|
127
133
|
```text
|
|
128
134
|
generate starting solution
|
|
129
|
-
└─ for each
|
|
135
|
+
└─ for each iteration / temperature step / improvisation
|
|
130
136
|
└─ perturb current specification → guaranteed distinct from current
|
|
131
137
|
├─ fit model with JAX-accelerated MLE
|
|
132
138
|
├─ run backward elimination (remove insignificant vars, refit)
|
|
133
|
-
├─ accept if converged +
|
|
134
|
-
|
|
139
|
+
├─ accept if converged + acceptance criterion satisfied
|
|
140
|
+
│ (Metropolis, dominance, or pitch adjustment)
|
|
141
|
+
└─ update best solution / harmony memory
|
|
135
142
|
print dashboard
|
|
136
143
|
```
|
|
137
144
|
|
|
@@ -433,27 +440,42 @@ python -m SearchLibrium --test_search_nest # run nested logit search
|
|
|
433
440
|
|
|
434
441
|
## Search algorithms
|
|
435
442
|
|
|
436
|
-
|
|
443
|
+
All algorithms share a **consistent interface**. The library auto-estimates
|
|
444
|
+
hyperparameters from problem size when you omit `ctrl`:
|
|
437
445
|
|
|
438
446
|
```python
|
|
439
|
-
from SearchLibrium import call_search,
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
#
|
|
446
|
-
best = call_search(params,
|
|
447
|
-
best = call_search(params, algorithm='
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
# Inspect auto-estimated
|
|
447
|
+
from SearchLibrium import (call_search, call_siman, call_harmony, call_sapbil,
|
|
448
|
+
call_banditsa, call_harmony_pbil,
|
|
449
|
+
call_parsa, call_parcopsa, estimate_ctrl)
|
|
450
|
+
|
|
451
|
+
# Unified entry point — auto hyperparameters (recommended)
|
|
452
|
+
best = call_search(params) # SA (default)
|
|
453
|
+
best = call_search(params, algorithm='hs') # Harmony Search
|
|
454
|
+
best = call_search(params, algorithm='sapbil') # SA + PBIL
|
|
455
|
+
best = call_search(params, algorithm='banditsa') # Bandit-guided SA
|
|
456
|
+
best = call_search(params, algorithm='hspbil') # HS + PBIL
|
|
457
|
+
|
|
458
|
+
# Inspect auto-estimated hyperparameters before running
|
|
451
459
|
ctrl = estimate_ctrl(params, algorithm='sa')
|
|
452
460
|
print(ctrl)
|
|
453
461
|
```
|
|
454
462
|
|
|
463
|
+
### Algorithm reference
|
|
464
|
+
|
|
465
|
+
| Function | Description | Population-based | Key idea |
|
|
466
|
+
| -------- | ----------- | :---: | -------- |
|
|
467
|
+
| `call_siman` | Simulated Annealing | | Metropolis acceptance with temperature cooling |
|
|
468
|
+
| `call_banditsa` | Bandit-guided SA | | Thompson Sampling to choose perturbation actions |
|
|
469
|
+
| `call_sapbil` | SA + PBIL | ✓ | Probability matrix learns from accepted solutions to bias perturbations |
|
|
470
|
+
| `call_harmony` | Harmony Search | ✓ | Memory-based improvisation with pitch adjustment |
|
|
471
|
+
| `call_harmony_pbil` | HS + PBIL | ✓ | Harmony Search with PBIL-guided pitch perturbation |
|
|
472
|
+
| `call_parsa` | Parallel SA | | Multiple independent SA solvers running in parallel |
|
|
473
|
+
| `call_parcopsa` | Parallel Cooperative SA | ✓ | Parallel SA with periodic best-solution sharing |
|
|
474
|
+
|
|
455
475
|
### Simulated Annealing (`call_siman` / `algorithm='sa'`)
|
|
456
476
|
|
|
477
|
+
Control tuple: `(tI, tF, max_temp_steps, max_iter)`
|
|
478
|
+
|
|
457
479
|
| Parameter | Meaning |
|
|
458
480
|
| --------- | ------- |
|
|
459
481
|
| `tI` | Initial temperature — higher → more exploration |
|
|
@@ -467,12 +489,14 @@ best = call_siman(params, ctrl=(1000, 0.001, 100, 20), id_num=1)
|
|
|
467
489
|
|
|
468
490
|
### Harmony Search (`call_harmony` / `algorithm='hs'`)
|
|
469
491
|
|
|
492
|
+
Control tuple: `(max_mem, maxiter, max_harm, min_harm, max_pitch, min_pitch)`
|
|
493
|
+
|
|
470
494
|
| Parameter | Meaning |
|
|
471
495
|
| --------- | ------- |
|
|
472
496
|
| `max_mem` | Harmony memory size (population) |
|
|
473
497
|
| `maxiter` | Improvisation iterations |
|
|
474
|
-
| `max_harm` | Max harmony consideration rate |
|
|
475
|
-
| `min_harm` | Min harmony consideration rate |
|
|
498
|
+
| `max_harm` | Max harmony memory consideration rate |
|
|
499
|
+
| `min_harm` | Min harmony memory consideration rate |
|
|
476
500
|
| `max_pitch` | Max pitch adjustment rate |
|
|
477
501
|
| `min_pitch` | Min pitch adjustment rate |
|
|
478
502
|
|
|
@@ -480,6 +504,44 @@ best = call_siman(params, ctrl=(1000, 0.001, 100, 20), id_num=1)
|
|
|
480
504
|
best = call_harmony(params, ctrl=(20, 400, 0.9, 0.6, 0.85, 0.3), id_num=1)
|
|
481
505
|
```
|
|
482
506
|
|
|
507
|
+
### SA + PBIL (`call_sapbil` / `algorithm='sapbil'`)
|
|
508
|
+
|
|
509
|
+
Population-Based Incremental Learning coupled with Simulated Annealing.
|
|
510
|
+
Maintains a probability matrix over the search decisions (inclusion,
|
|
511
|
+
randomness, distribution, correlation, Box-Cox) and updates it from
|
|
512
|
+
accepted solutions using a temperature-coupled learning rate.
|
|
513
|
+
|
|
514
|
+
```python
|
|
515
|
+
best = call_sapbil(params, id_num=1)
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### HS + PBIL (`call_harmony_pbil` / `algorithm='hspbil'`)
|
|
519
|
+
|
|
520
|
+
Harmony Search with PBIL-guided pitch adjustment. Instead of uniform-random
|
|
521
|
+
perturbations during pitch adjustment, the PBIL probability matrix guides
|
|
522
|
+
add/remove decisions based on learned probabilities.
|
|
523
|
+
|
|
524
|
+
```python
|
|
525
|
+
best = call_harmony_pbil(params, id_num=1)
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### Bandit-guided SA (`call_banditsa` / `algorithm='banditsa'`)
|
|
529
|
+
|
|
530
|
+
Uses Thompson Sampling over a multi-armed bandit of perturbation actions
|
|
531
|
+
(add asvar, remove asvar, change distribution, ...). Actions that
|
|
532
|
+
historically led to accepted solutions are more likely to be chosen again.
|
|
533
|
+
|
|
534
|
+
```python
|
|
535
|
+
best = call_banditsa(params, id_num=1)
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
### Parallel SA variants
|
|
539
|
+
|
|
540
|
+
```python
|
|
541
|
+
best = call_parsa(params, nthrds=4) # 4 independent SA solvers
|
|
542
|
+
best = call_parcopsa(params, nthrds=8) # 8 SA solvers sharing best periodically
|
|
543
|
+
```
|
|
544
|
+
|
|
483
545
|
### Auto hyperparameter estimation
|
|
484
546
|
|
|
485
547
|
If `ctrl` is omitted, the library estimates appropriate defaults from the
|
|
@@ -489,8 +551,6 @@ problem complexity (`n_vars × n_alts × n_models`, doubled for random params):
|
|
|
489
551
|
from SearchLibrium import estimate_ctrl
|
|
490
552
|
ctrl_sa = estimate_ctrl(params, algorithm='sa')
|
|
491
553
|
ctrl_hs = estimate_ctrl(params, algorithm='hs')
|
|
492
|
-
print('SA ctrl:', ctrl_sa)
|
|
493
|
-
print('HS ctrl:', ctrl_hs)
|
|
494
554
|
```
|
|
495
555
|
|
|
496
556
|
Complexity buckets:
|
|
@@ -504,6 +564,64 @@ Complexity buckets:
|
|
|
504
564
|
|
|
505
565
|
|
|
506
566
|
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
## Constraints
|
|
570
|
+
|
|
571
|
+
Use `ConstraintBuilder` to enforce model structure rules during the search:
|
|
572
|
+
|
|
573
|
+
```python
|
|
574
|
+
from SearchLibrium import Parameters, ConstraintBuilder, call_siman
|
|
575
|
+
|
|
576
|
+
constraints = ConstraintBuilder()
|
|
577
|
+
|
|
578
|
+
# ── Inclusion / exclusion ──
|
|
579
|
+
constraints.force_include('TIME', 'COST') # must always appear
|
|
580
|
+
constraints.never_include('ID', 'DUMMY') # must never appear
|
|
581
|
+
|
|
582
|
+
# ── Mutually exclusive groups ──
|
|
583
|
+
# At most ONE variable from each group may appear in any solution.
|
|
584
|
+
# The search will never add a partner if a group member is already
|
|
585
|
+
# present, and will remove extras if they slip through.
|
|
586
|
+
constraints.mutually_exclusive('SPEED', 'TIME') # speed OR time, never both
|
|
587
|
+
constraints.mutually_exclusive('LOG_INCOME', 'INCOME_DUMMY') # independent group
|
|
588
|
+
|
|
589
|
+
# ── Minimum behavioural content ──
|
|
590
|
+
# Require at least N variables from a pool without locking in which ones.
|
|
591
|
+
constraints.min_behavioral(2, 'PRICE', 'BIKELANE', 'DIST6', 'RECRE')
|
|
592
|
+
|
|
593
|
+
# ── Random-parameter rules ──
|
|
594
|
+
constraints.force_random('TIME', distribution='n') # must have random parameter
|
|
595
|
+
constraints.exclude_random('HEADWAY') # must never be random
|
|
596
|
+
|
|
597
|
+
# ── Attach constraints to the Parameters object ──
|
|
598
|
+
params = Parameters(
|
|
599
|
+
criterions = [("bic", -1)],
|
|
600
|
+
df = df,
|
|
601
|
+
varnames = varnames,
|
|
602
|
+
asvarnames = varnames,
|
|
603
|
+
choice_set = choice_set,
|
|
604
|
+
choices = df["CHOICE"].values,
|
|
605
|
+
alt_var = df["alt"].values,
|
|
606
|
+
choice_id = df["custom_id"].values,
|
|
607
|
+
ind_id = df["ID"].values,
|
|
608
|
+
base_alt = "SM",
|
|
609
|
+
models = ["multinomial", "mixed_logit"],
|
|
610
|
+
allow_random = True,
|
|
611
|
+
p_val = 0.05,
|
|
612
|
+
pre_spec_constraints = constraints.dict(), # ◄ attach here
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
best = call_siman(params, id_num=1)
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
The constraint enforcement happens at every stage:
|
|
619
|
+
- **Solution generation** — filtered so violations are never created
|
|
620
|
+
- **Perturbation / mutation** — blocked from adding violating variables
|
|
621
|
+
- **Post-evaluation cleanup** — extra members are removed if somehow present
|
|
622
|
+
|
|
623
|
+
---
|
|
624
|
+
|
|
507
625
|
## License
|
|
508
626
|
|
|
509
627
|
MIT — see [LICENSE](LICENSE) for details.
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
[](https://pypi.org/project/SearchLibrium/)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
[](https://github.com/zahern/HypothesisX/actions/workflows/ci.yml)
|
|
7
|
+
[](https://github.com/zahern/HypothesisX)
|
|
7
8
|
|
|
8
|
-
**Automated discrete choice model search powered by Simulated Annealing, Harmony Search, and JAX-accelerated MLE.**
|
|
9
|
+
**Automated discrete choice model search powered by Simulated Annealing, Bandit-guided SA, Harmony Search, SA+PBIL, HS+PBIL, Parallel SA, and JAX-accelerated MLE.**
|
|
9
10
|
|
|
10
11
|
SearchLibrium searches over model specifications — which variables to include, whether parameters should be random, which transformations to apply, and which model class to use — and returns the best converged, all-significant model according to your chosen criterion (BIC, AIC, log-likelihood, MAE, or multi-objective combinations).
|
|
11
12
|
|
|
@@ -85,16 +86,19 @@ A **run dashboard** is printed automatically at the end of every search, showing
|
|
|
85
86
|
|
|
86
87
|
## How the search works
|
|
87
88
|
|
|
88
|
-
The search uses **
|
|
89
|
+
The search uses **metaheuristic optimisation** (SA, BanditSA, Harmony Search,
|
|
90
|
+
SA+PBIL, HS+PBIL, or parallel variants) to explore the space of model
|
|
91
|
+
specifications:
|
|
89
92
|
|
|
90
93
|
```text
|
|
91
94
|
generate starting solution
|
|
92
|
-
└─ for each
|
|
95
|
+
└─ for each iteration / temperature step / improvisation
|
|
93
96
|
└─ perturb current specification → guaranteed distinct from current
|
|
94
97
|
├─ fit model with JAX-accelerated MLE
|
|
95
98
|
├─ run backward elimination (remove insignificant vars, refit)
|
|
96
|
-
├─ accept if converged +
|
|
97
|
-
|
|
99
|
+
├─ accept if converged + acceptance criterion satisfied
|
|
100
|
+
│ (Metropolis, dominance, or pitch adjustment)
|
|
101
|
+
└─ update best solution / harmony memory
|
|
98
102
|
print dashboard
|
|
99
103
|
```
|
|
100
104
|
|
|
@@ -396,27 +400,42 @@ python -m SearchLibrium --test_search_nest # run nested logit search
|
|
|
396
400
|
|
|
397
401
|
## Search algorithms
|
|
398
402
|
|
|
399
|
-
|
|
403
|
+
All algorithms share a **consistent interface**. The library auto-estimates
|
|
404
|
+
hyperparameters from problem size when you omit `ctrl`:
|
|
400
405
|
|
|
401
406
|
```python
|
|
402
|
-
from SearchLibrium import call_search,
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
#
|
|
409
|
-
best = call_search(params,
|
|
410
|
-
best = call_search(params, algorithm='
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
# Inspect auto-estimated
|
|
407
|
+
from SearchLibrium import (call_search, call_siman, call_harmony, call_sapbil,
|
|
408
|
+
call_banditsa, call_harmony_pbil,
|
|
409
|
+
call_parsa, call_parcopsa, estimate_ctrl)
|
|
410
|
+
|
|
411
|
+
# Unified entry point — auto hyperparameters (recommended)
|
|
412
|
+
best = call_search(params) # SA (default)
|
|
413
|
+
best = call_search(params, algorithm='hs') # Harmony Search
|
|
414
|
+
best = call_search(params, algorithm='sapbil') # SA + PBIL
|
|
415
|
+
best = call_search(params, algorithm='banditsa') # Bandit-guided SA
|
|
416
|
+
best = call_search(params, algorithm='hspbil') # HS + PBIL
|
|
417
|
+
|
|
418
|
+
# Inspect auto-estimated hyperparameters before running
|
|
414
419
|
ctrl = estimate_ctrl(params, algorithm='sa')
|
|
415
420
|
print(ctrl)
|
|
416
421
|
```
|
|
417
422
|
|
|
423
|
+
### Algorithm reference
|
|
424
|
+
|
|
425
|
+
| Function | Description | Population-based | Key idea |
|
|
426
|
+
| -------- | ----------- | :---: | -------- |
|
|
427
|
+
| `call_siman` | Simulated Annealing | | Metropolis acceptance with temperature cooling |
|
|
428
|
+
| `call_banditsa` | Bandit-guided SA | | Thompson Sampling to choose perturbation actions |
|
|
429
|
+
| `call_sapbil` | SA + PBIL | ✓ | Probability matrix learns from accepted solutions to bias perturbations |
|
|
430
|
+
| `call_harmony` | Harmony Search | ✓ | Memory-based improvisation with pitch adjustment |
|
|
431
|
+
| `call_harmony_pbil` | HS + PBIL | ✓ | Harmony Search with PBIL-guided pitch perturbation |
|
|
432
|
+
| `call_parsa` | Parallel SA | | Multiple independent SA solvers running in parallel |
|
|
433
|
+
| `call_parcopsa` | Parallel Cooperative SA | ✓ | Parallel SA with periodic best-solution sharing |
|
|
434
|
+
|
|
418
435
|
### Simulated Annealing (`call_siman` / `algorithm='sa'`)
|
|
419
436
|
|
|
437
|
+
Control tuple: `(tI, tF, max_temp_steps, max_iter)`
|
|
438
|
+
|
|
420
439
|
| Parameter | Meaning |
|
|
421
440
|
| --------- | ------- |
|
|
422
441
|
| `tI` | Initial temperature — higher → more exploration |
|
|
@@ -430,12 +449,14 @@ best = call_siman(params, ctrl=(1000, 0.001, 100, 20), id_num=1)
|
|
|
430
449
|
|
|
431
450
|
### Harmony Search (`call_harmony` / `algorithm='hs'`)
|
|
432
451
|
|
|
452
|
+
Control tuple: `(max_mem, maxiter, max_harm, min_harm, max_pitch, min_pitch)`
|
|
453
|
+
|
|
433
454
|
| Parameter | Meaning |
|
|
434
455
|
| --------- | ------- |
|
|
435
456
|
| `max_mem` | Harmony memory size (population) |
|
|
436
457
|
| `maxiter` | Improvisation iterations |
|
|
437
|
-
| `max_harm` | Max harmony consideration rate |
|
|
438
|
-
| `min_harm` | Min harmony consideration rate |
|
|
458
|
+
| `max_harm` | Max harmony memory consideration rate |
|
|
459
|
+
| `min_harm` | Min harmony memory consideration rate |
|
|
439
460
|
| `max_pitch` | Max pitch adjustment rate |
|
|
440
461
|
| `min_pitch` | Min pitch adjustment rate |
|
|
441
462
|
|
|
@@ -443,6 +464,44 @@ best = call_siman(params, ctrl=(1000, 0.001, 100, 20), id_num=1)
|
|
|
443
464
|
best = call_harmony(params, ctrl=(20, 400, 0.9, 0.6, 0.85, 0.3), id_num=1)
|
|
444
465
|
```
|
|
445
466
|
|
|
467
|
+
### SA + PBIL (`call_sapbil` / `algorithm='sapbil'`)
|
|
468
|
+
|
|
469
|
+
Population-Based Incremental Learning coupled with Simulated Annealing.
|
|
470
|
+
Maintains a probability matrix over the search decisions (inclusion,
|
|
471
|
+
randomness, distribution, correlation, Box-Cox) and updates it from
|
|
472
|
+
accepted solutions using a temperature-coupled learning rate.
|
|
473
|
+
|
|
474
|
+
```python
|
|
475
|
+
best = call_sapbil(params, id_num=1)
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### HS + PBIL (`call_harmony_pbil` / `algorithm='hspbil'`)
|
|
479
|
+
|
|
480
|
+
Harmony Search with PBIL-guided pitch adjustment. Instead of uniform-random
|
|
481
|
+
perturbations during pitch adjustment, the PBIL probability matrix guides
|
|
482
|
+
add/remove decisions based on learned probabilities.
|
|
483
|
+
|
|
484
|
+
```python
|
|
485
|
+
best = call_harmony_pbil(params, id_num=1)
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
### Bandit-guided SA (`call_banditsa` / `algorithm='banditsa'`)
|
|
489
|
+
|
|
490
|
+
Uses Thompson Sampling over a multi-armed bandit of perturbation actions
|
|
491
|
+
(add asvar, remove asvar, change distribution, ...). Actions that
|
|
492
|
+
historically led to accepted solutions are more likely to be chosen again.
|
|
493
|
+
|
|
494
|
+
```python
|
|
495
|
+
best = call_banditsa(params, id_num=1)
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
### Parallel SA variants
|
|
499
|
+
|
|
500
|
+
```python
|
|
501
|
+
best = call_parsa(params, nthrds=4) # 4 independent SA solvers
|
|
502
|
+
best = call_parcopsa(params, nthrds=8) # 8 SA solvers sharing best periodically
|
|
503
|
+
```
|
|
504
|
+
|
|
446
505
|
### Auto hyperparameter estimation
|
|
447
506
|
|
|
448
507
|
If `ctrl` is omitted, the library estimates appropriate defaults from the
|
|
@@ -452,8 +511,6 @@ problem complexity (`n_vars × n_alts × n_models`, doubled for random params):
|
|
|
452
511
|
from SearchLibrium import estimate_ctrl
|
|
453
512
|
ctrl_sa = estimate_ctrl(params, algorithm='sa')
|
|
454
513
|
ctrl_hs = estimate_ctrl(params, algorithm='hs')
|
|
455
|
-
print('SA ctrl:', ctrl_sa)
|
|
456
|
-
print('HS ctrl:', ctrl_hs)
|
|
457
514
|
```
|
|
458
515
|
|
|
459
516
|
Complexity buckets:
|
|
@@ -467,6 +524,64 @@ Complexity buckets:
|
|
|
467
524
|
|
|
468
525
|
|
|
469
526
|
|
|
527
|
+
---
|
|
528
|
+
|
|
529
|
+
## Constraints
|
|
530
|
+
|
|
531
|
+
Use `ConstraintBuilder` to enforce model structure rules during the search:
|
|
532
|
+
|
|
533
|
+
```python
|
|
534
|
+
from SearchLibrium import Parameters, ConstraintBuilder, call_siman
|
|
535
|
+
|
|
536
|
+
constraints = ConstraintBuilder()
|
|
537
|
+
|
|
538
|
+
# ── Inclusion / exclusion ──
|
|
539
|
+
constraints.force_include('TIME', 'COST') # must always appear
|
|
540
|
+
constraints.never_include('ID', 'DUMMY') # must never appear
|
|
541
|
+
|
|
542
|
+
# ── Mutually exclusive groups ──
|
|
543
|
+
# At most ONE variable from each group may appear in any solution.
|
|
544
|
+
# The search will never add a partner if a group member is already
|
|
545
|
+
# present, and will remove extras if they slip through.
|
|
546
|
+
constraints.mutually_exclusive('SPEED', 'TIME') # speed OR time, never both
|
|
547
|
+
constraints.mutually_exclusive('LOG_INCOME', 'INCOME_DUMMY') # independent group
|
|
548
|
+
|
|
549
|
+
# ── Minimum behavioural content ──
|
|
550
|
+
# Require at least N variables from a pool without locking in which ones.
|
|
551
|
+
constraints.min_behavioral(2, 'PRICE', 'BIKELANE', 'DIST6', 'RECRE')
|
|
552
|
+
|
|
553
|
+
# ── Random-parameter rules ──
|
|
554
|
+
constraints.force_random('TIME', distribution='n') # must have random parameter
|
|
555
|
+
constraints.exclude_random('HEADWAY') # must never be random
|
|
556
|
+
|
|
557
|
+
# ── Attach constraints to the Parameters object ──
|
|
558
|
+
params = Parameters(
|
|
559
|
+
criterions = [("bic", -1)],
|
|
560
|
+
df = df,
|
|
561
|
+
varnames = varnames,
|
|
562
|
+
asvarnames = varnames,
|
|
563
|
+
choice_set = choice_set,
|
|
564
|
+
choices = df["CHOICE"].values,
|
|
565
|
+
alt_var = df["alt"].values,
|
|
566
|
+
choice_id = df["custom_id"].values,
|
|
567
|
+
ind_id = df["ID"].values,
|
|
568
|
+
base_alt = "SM",
|
|
569
|
+
models = ["multinomial", "mixed_logit"],
|
|
570
|
+
allow_random = True,
|
|
571
|
+
p_val = 0.05,
|
|
572
|
+
pre_spec_constraints = constraints.dict(), # ◄ attach here
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
best = call_siman(params, id_num=1)
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
The constraint enforcement happens at every stage:
|
|
579
|
+
- **Solution generation** — filtered so violations are never created
|
|
580
|
+
- **Perturbation / mutation** — blocked from adding violating variables
|
|
581
|
+
- **Post-evaluation cleanup** — extra members are removed if somehow present
|
|
582
|
+
|
|
583
|
+
---
|
|
584
|
+
|
|
470
585
|
## License
|
|
471
586
|
|
|
472
587
|
MIT — see [LICENSE](LICENSE) for details.
|
|
@@ -65,12 +65,15 @@ dev = [
|
|
|
65
65
|
|
|
66
66
|
[project.urls]
|
|
67
67
|
Homepage = "https://pypi.org/project/SearchLibrium/"
|
|
68
|
+
Repository = "https://github.com/zahern/HypothesisX"
|
|
69
|
+
Issues = "https://github.com/zahern/HypothesisX/issues"
|
|
70
|
+
Documentation = "https://pypi.org/project/SearchLibrium/"
|
|
68
71
|
|
|
69
72
|
[project.scripts]
|
|
70
73
|
realpython = "SearchLibrium.__main__:main"
|
|
71
74
|
|
|
72
75
|
[tool.bumpver]
|
|
73
|
-
current_version = "0.0.
|
|
76
|
+
current_version = "0.0.129"
|
|
74
77
|
version_pattern = "MAJOR.MINOR.PATCH"
|
|
75
78
|
commit_message = "[skip ci] Bump version {old_version} -> {new_version}"
|
|
76
79
|
commit = true
|
|
@@ -113,7 +113,7 @@ try:
|
|
|
113
113
|
from . import misc
|
|
114
114
|
from .sapbil import SAPBIL, ProbabilityMatrix
|
|
115
115
|
from .hspbil import HSPBIL
|
|
116
|
-
from .call_meta import call_harmony, call_harmony_pbil, call_siman, call_parsa, call_search, call_sapbil, estimate_ctrl
|
|
116
|
+
from .call_meta import call_harmony, call_harmony_pbil, call_siman, call_parsa, call_search, call_sapbil, call_banditsa, call_parcopsa, estimate_ctrl
|
|
117
117
|
from .predict import DestinationPredictor, predict_destination_components, predict_aggregate_destination_flows
|
|
118
118
|
|
|
119
119
|
except ImportError as e:
|
|
@@ -135,7 +135,7 @@ except ImportError as e:
|
|
|
135
135
|
from search import Parameters
|
|
136
136
|
from sapbil import SAPBIL, ProbabilityMatrix
|
|
137
137
|
from hspbil import HSPBIL
|
|
138
|
-
from call_meta import call_siman, call_harmony, call_harmony_pbil, call_search, call_sapbil, estimate_ctrl
|
|
138
|
+
from call_meta import call_siman, call_harmony, call_harmony_pbil, call_search, call_sapbil, call_banditsa, call_parsa, call_parcopsa, estimate_ctrl
|
|
139
139
|
from predict import DestinationPredictor, predict_destination_components, predict_aggregate_destination_flows
|
|
140
140
|
try:
|
|
141
141
|
from .main import print_ascii_art_logo
|
|
@@ -705,12 +705,334 @@ class HarmonySearch(Search):
|
|
|
705
705
|
# }
|
|
706
706
|
# }
|
|
707
707
|
|
|
708
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
709
|
+
# TAILORED PERTURBATIONS — model-development-style moves (changes 11–18)
|
|
710
|
+
# These mirror how a practitioner actually builds discrete choice models:
|
|
711
|
+
# significance pruning, distribution sweeps, structural hypothesis tests,
|
|
712
|
+
# model progression, ensemble crossover, and feature substitution.
|
|
713
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
714
|
+
|
|
715
|
+
def make_change_11_significance_prune(self, candidate):
|
|
716
|
+
"""Model-developer move: remove the least significant variable from
|
|
717
|
+
the best solution (mirrors backward elimination). Then try adding
|
|
718
|
+
the most promising unused variable as compensation."""
|
|
719
|
+
best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, \
|
|
720
|
+
asc_ind, _, _ = self.get_best_features(candidate)
|
|
721
|
+
|
|
722
|
+
# Only prune if we have > 2 AS vars (keep a minimum)
|
|
723
|
+
if len(best_asvars) <= 2:
|
|
724
|
+
return
|
|
725
|
+
|
|
726
|
+
# ── Try removing one AS variable ──────────────────────────────
|
|
727
|
+
candidates_to_drop = [v for v in best_asvars
|
|
728
|
+
if v not in self._get_forced_vars()]
|
|
729
|
+
if not candidates_to_drop:
|
|
730
|
+
return
|
|
731
|
+
|
|
732
|
+
# Try each candidate drop (up to 3) — a real modeller tests each removal
|
|
733
|
+
for drop_var in candidates_to_drop[:3]:
|
|
734
|
+
trial_vars = [v for v in best_asvars if v != drop_var]
|
|
735
|
+
trial_randvars = {k: v for k, v in best_randvars.items()
|
|
736
|
+
if k in trial_vars}
|
|
737
|
+
solution = Solution(nb_crit=self.nb_crit, asvars=trial_vars,
|
|
738
|
+
isvars=best_isvars, randvars=trial_randvars,
|
|
739
|
+
bcvars=best_bcvars, corvars=best_corvars,
|
|
740
|
+
asc_ind=asc_ind)
|
|
741
|
+
solution['model_n'] = candidate[0]['model_n'] if isinstance(candidate, list) and candidate else None
|
|
742
|
+
revised, converged = self.evaluate_solution(solution)
|
|
743
|
+
if converged:
|
|
744
|
+
self.insert_solution(revised)
|
|
745
|
+
|
|
746
|
+
# ── Then try adding a new promising variable ──────────────────
|
|
747
|
+
unused = [v for v in self.param.avail_asvars
|
|
748
|
+
if v not in best_asvars and v not in self.param.ps_asvars]
|
|
749
|
+
if unused:
|
|
750
|
+
add_var = self.param.generator.choice(unused)
|
|
751
|
+
trial_vars = best_asvars + [add_var]
|
|
752
|
+
trial_vars = list(dict.fromkeys(trial_vars))
|
|
753
|
+
solution = Solution(nb_crit=self.nb_crit, asvars=trial_vars,
|
|
754
|
+
isvars=best_isvars, randvars=best_randvars,
|
|
755
|
+
bcvars=best_bcvars, corvars=best_corvars,
|
|
756
|
+
asc_ind=asc_ind)
|
|
757
|
+
revised, converged = self.evaluate_solution(solution)
|
|
758
|
+
if converged:
|
|
759
|
+
self.insert_solution(revised)
|
|
760
|
+
|
|
761
|
+
def make_change_12_distribution_sweep(self, candidate):
|
|
762
|
+
"""Model-developer move: for each random parameter in the best
|
|
763
|
+
solution, test every alternative distribution. A practitioner
|
|
764
|
+
often asks 'is this log-normal or should it be triangular?'"""
|
|
765
|
+
best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, \
|
|
766
|
+
asc_ind, _, _ = self.get_best_features(candidate)
|
|
767
|
+
|
|
768
|
+
if not best_randvars or len(best_randvars) == 0:
|
|
769
|
+
return
|
|
770
|
+
|
|
771
|
+
all_distributions = [d for d in self.param.distr if d != 'f']
|
|
772
|
+
|
|
773
|
+
for var_name, current_dist in list(best_randvars.items()):
|
|
774
|
+
alternatives = [d for d in all_distributions if d != current_dist]
|
|
775
|
+
if not alternatives:
|
|
776
|
+
continue
|
|
777
|
+
for alt_dist in alternatives[:len(alternatives)]:
|
|
778
|
+
trial_randvars = dict(best_randvars)
|
|
779
|
+
trial_randvars[var_name] = alt_dist
|
|
780
|
+
solution = Solution(nb_crit=self.nb_crit, asvars=best_asvars,
|
|
781
|
+
isvars=best_isvars, randvars=trial_randvars,
|
|
782
|
+
bcvars=best_bcvars, corvars=best_corvars,
|
|
783
|
+
asc_ind=asc_ind)
|
|
784
|
+
solution['model_n'] = candidate[0]['model_n'] if isinstance(candidate, list) and candidate else 'mixed_logit'
|
|
785
|
+
revised, converged = self.evaluate_solution(solution)
|
|
786
|
+
if converged:
|
|
787
|
+
self.insert_solution(revised)
|
|
788
|
+
|
|
789
|
+
def make_change_13_model_progression(self, candidate):
|
|
790
|
+
"""Model-developer move: systematically step up or down the model
|
|
791
|
+
complexity ladder: MNL → NestedLogit → MixedLogit → MixedNested.
|
|
792
|
+
A modeller tests 'does adding nesting improve fit? does adding
|
|
793
|
+
random heterogeneity after nesting improve it further?'"""
|
|
794
|
+
best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, \
|
|
795
|
+
asc_ind, _, _ = self.get_best_features(candidate)
|
|
796
|
+
|
|
797
|
+
current_model = (candidate[0].get('model_n', 'multinomial')
|
|
798
|
+
if isinstance(candidate, list) and candidate
|
|
799
|
+
else 'multinomial')
|
|
800
|
+
|
|
801
|
+
has_nests = bool(getattr(self.param, 'nests', None))
|
|
802
|
+
has_randvars = bool(best_randvars)
|
|
803
|
+
|
|
804
|
+
# Model ladder — try the "next step up" in complexity
|
|
805
|
+
progression = []
|
|
806
|
+
if has_nests:
|
|
807
|
+
progression = ['multinomial', 'nested_logit', 'mixed_nested']
|
|
808
|
+
else:
|
|
809
|
+
progression = ['multinomial', 'mixed_logit']
|
|
810
|
+
|
|
811
|
+
# Also consider stepping down (for parsimony)
|
|
812
|
+
for target_model in progression:
|
|
813
|
+
if target_model == current_model:
|
|
814
|
+
continue
|
|
815
|
+
# mixed_nested requires both nests and random params
|
|
816
|
+
if target_model == 'mixed_nested' and not (has_nests and has_randvars):
|
|
817
|
+
continue
|
|
818
|
+
solution = Solution(nb_crit=self.nb_crit, asvars=best_asvars,
|
|
819
|
+
isvars=best_isvars, randvars=best_randvars,
|
|
820
|
+
bcvars=best_bcvars, corvars=best_corvars,
|
|
821
|
+
asc_ind=asc_ind, model_n=target_model)
|
|
822
|
+
revised, converged = self.evaluate_solution(solution)
|
|
823
|
+
if converged:
|
|
824
|
+
self.insert_solution(revised)
|
|
825
|
+
|
|
826
|
+
def make_change_14_ensemble_crossover(self, candidate):
|
|
827
|
+
"""Model-developer move: combine variables from two top solutions
|
|
828
|
+
(like a modeller synthesising insights from two promising specs).
|
|
829
|
+
Creates four offspring: union, intersection, top-only, second-only."""
|
|
830
|
+
if len(self.memory) < 2:
|
|
831
|
+
return
|
|
832
|
+
|
|
833
|
+
memory_sorted = self.sort_memory(self.memory[:])
|
|
834
|
+
sol_a = memory_sorted[0]
|
|
835
|
+
sol_b = memory_sorted[1]
|
|
836
|
+
|
|
837
|
+
a_asvars = set(sol_a.get('asvars', []))
|
|
838
|
+
b_asvars = set(sol_b.get('asvars', []))
|
|
839
|
+
a_randvars = sol_a.get('randvars', {})
|
|
840
|
+
b_randvars = sol_b.get('randvars', {})
|
|
841
|
+
|
|
842
|
+
# ── Offspring 1: Union of both specs ──
|
|
843
|
+
union_vars = sorted(a_asvars | b_asvars)
|
|
844
|
+
if union_vars:
|
|
845
|
+
merged_randvars = {}
|
|
846
|
+
for v in union_vars:
|
|
847
|
+
if v in a_randvars: merged_randvars[v] = a_randvars[v]
|
|
848
|
+
elif v in b_randvars: merged_randvars[v] = b_randvars[v]
|
|
849
|
+
solution = Solution(nb_crit=self.nb_crit, asvars=union_vars,
|
|
850
|
+
isvars=sol_a.get('isvars', []),
|
|
851
|
+
randvars=merged_randvars,
|
|
852
|
+
bcvars=[], corvars=[],
|
|
853
|
+
asc_ind=sol_a.get('asc_ind', False))
|
|
854
|
+
revised, converged = self.evaluate_solution(solution)
|
|
855
|
+
if converged:
|
|
856
|
+
self.insert_solution(revised)
|
|
857
|
+
|
|
858
|
+
# ── Offspring 2: Intersection (core stable variables) ──
|
|
859
|
+
inter_vars = sorted(a_asvars & b_asvars)
|
|
860
|
+
if inter_vars and len(inter_vars) >= 2:
|
|
861
|
+
inter_randvars = {v: a_randvars[v] for v in inter_vars
|
|
862
|
+
if v in a_randvars}
|
|
863
|
+
solution = Solution(nb_crit=self.nb_crit, asvars=inter_vars,
|
|
864
|
+
isvars=sol_a.get('isvars', []),
|
|
865
|
+
randvars=inter_randvars,
|
|
866
|
+
bcvars=[], corvars=[],
|
|
867
|
+
asc_ind=sol_a.get('asc_ind', False))
|
|
868
|
+
revised, converged = self.evaluate_solution(solution)
|
|
869
|
+
if converged:
|
|
870
|
+
self.insert_solution(revised)
|
|
871
|
+
|
|
872
|
+
def make_change_15_feature_substitution(self, candidate):
|
|
873
|
+
"""Model-developer move: substitute one base feature for a
|
|
874
|
+
correlated alternative. E.g. swap ln_dest_trips for
|
|
875
|
+
osm_destination_pull, or swap ln_dist for a spline/knot."""
|
|
876
|
+
best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, \
|
|
877
|
+
asc_ind, _, _ = self.get_best_features(candidate)
|
|
878
|
+
|
|
879
|
+
# Pre-check: identify substitution groups among available variables
|
|
880
|
+
# (variables that measure the same construct differently)
|
|
881
|
+
subst_groups = self._build_substitution_groups(best_asvars)
|
|
882
|
+
|
|
883
|
+
if not subst_groups:
|
|
884
|
+
return
|
|
885
|
+
|
|
886
|
+
for original, replacement in subst_groups:
|
|
887
|
+
trial_vars = [replacement if v == original else v
|
|
888
|
+
for v in best_asvars]
|
|
889
|
+
trial_randvars = {replacement if k == original else k: v
|
|
890
|
+
for k, v in best_randvars.items()}
|
|
891
|
+
solution = Solution(nb_crit=self.nb_crit, asvars=trial_vars,
|
|
892
|
+
isvars=best_isvars, randvars=trial_randvars,
|
|
893
|
+
bcvars=best_bcvars, corvars=best_corvars,
|
|
894
|
+
asc_ind=asc_ind)
|
|
895
|
+
solution['model_n'] = candidate[0]['model_n'] if isinstance(candidate, list) and candidate else None
|
|
896
|
+
revised, converged = self.evaluate_solution(solution)
|
|
897
|
+
if converged:
|
|
898
|
+
self.insert_solution(revised)
|
|
899
|
+
|
|
900
|
+
def make_change_16_structural_hypothesis(self, candidate):
|
|
901
|
+
"""Model-developer move: test a structural hypothesis by taking
|
|
902
|
+
one origin profile and sweeping across ALL destination attributes.
|
|
903
|
+
'Does bike ownership moderate sensitivity to every destination
|
|
904
|
+
feature, or just distance?'"""
|
|
905
|
+
best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, \
|
|
906
|
+
asc_ind, _, _ = self.get_best_features(candidate)
|
|
907
|
+
|
|
908
|
+
# Find which origin profiles are currently used via interactions
|
|
909
|
+
active_origins = set()
|
|
910
|
+
for v in best_asvars:
|
|
911
|
+
if v.startswith('inter__prop_'):
|
|
912
|
+
parts = v.replace('inter__', '').split('__')
|
|
913
|
+
if len(parts) >= 1:
|
|
914
|
+
active_origins.add(parts[0])
|
|
915
|
+
|
|
916
|
+
if not active_origins:
|
|
917
|
+
return
|
|
918
|
+
|
|
919
|
+
# Pick one active origin profile to sweep
|
|
920
|
+
origins_list = sorted(active_origins)
|
|
921
|
+
origin = self.param.generator.choice(origins_list)
|
|
922
|
+
|
|
923
|
+
# For this origin, add interactions with every OTHER destination attr
|
|
924
|
+
# that isn't already present (structural completeness hypothesis)
|
|
925
|
+
for var_name in self.param.avail_asvars:
|
|
926
|
+
if not var_name.startswith('inter__'):
|
|
927
|
+
continue
|
|
928
|
+
if var_name in best_asvars:
|
|
929
|
+
continue
|
|
930
|
+
if origin not in var_name:
|
|
931
|
+
continue
|
|
932
|
+
|
|
933
|
+
trial_vars = best_asvars + [var_name]
|
|
934
|
+
trial_vars = list(dict.fromkeys(trial_vars))
|
|
935
|
+
trial_vars = self.remove_collinear_vars(trial_vars)
|
|
936
|
+
solution = Solution(nb_crit=self.nb_crit, asvars=trial_vars,
|
|
937
|
+
isvars=best_isvars, randvars=best_randvars,
|
|
938
|
+
bcvars=best_bcvars, corvars=best_corvars,
|
|
939
|
+
asc_ind=asc_ind)
|
|
940
|
+
revised, converged = self.evaluate_solution(solution)
|
|
941
|
+
if converged:
|
|
942
|
+
self.insert_solution(revised)
|
|
943
|
+
|
|
944
|
+
def _build_substitution_groups(self, asvars):
|
|
945
|
+
"""Identify groups of variables that measure similar constructs
|
|
946
|
+
and can be substituted for one another."""
|
|
947
|
+
groups = []
|
|
948
|
+
# Group 1: destination size measures
|
|
949
|
+
size_measures = [v for v in asvars
|
|
950
|
+
if v in {'ln_dest_trips', 'osm_destination_pull',
|
|
951
|
+
'acdc_aggl', 'osm_poi_intensity'}]
|
|
952
|
+
for i in range(len(size_measures)):
|
|
953
|
+
for j in range(i + 1, len(size_measures)):
|
|
954
|
+
groups.append((size_measures[i], size_measures[j]))
|
|
955
|
+
|
|
956
|
+
# Group 2: distance-related measures
|
|
957
|
+
dist_measures = [v for v in asvars
|
|
958
|
+
if v in {'ln_dist', 'dist_km',
|
|
959
|
+
'acdc_nearest_dest_km', 'acdc_weighted_mean_dist_km'}]
|
|
960
|
+
for i in range(len(dist_measures)):
|
|
961
|
+
for j in range(i + 1, len(dist_measures)):
|
|
962
|
+
groups.append((dist_measures[i], dist_measures[j]))
|
|
963
|
+
|
|
964
|
+
# Group 3: OSM amenity measures
|
|
965
|
+
osm_measures = [v for v in asvars
|
|
966
|
+
if v.startswith('osm_') and not v.startswith('osm_dest')]
|
|
967
|
+
for i in range(len(osm_measures)):
|
|
968
|
+
for j in range(i + 1, len(osm_measures)):
|
|
969
|
+
groups.append((osm_measures[i], osm_measures[j]))
|
|
970
|
+
|
|
971
|
+
return groups
|
|
972
|
+
|
|
973
|
+
def _get_forced_vars(self):
|
|
974
|
+
"""Return list of variables that must never be removed."""
|
|
975
|
+
forced = set()
|
|
976
|
+
forced.update(getattr(self.param, 'ps_asvars', []))
|
|
977
|
+
forced.update(getattr(self.param, 'ps_isvars', []))
|
|
978
|
+
if hasattr(self.param, 'pres_spec_constr') and self.param.pres_spec_constr:
|
|
979
|
+
forced.update(self.param.pres_spec_constr.get('force_include', []))
|
|
980
|
+
return forced
|
|
981
|
+
|
|
982
|
+
def local_search_developer(self, iter: int, phase: str = 'early'):
|
|
983
|
+
"""
|
|
984
|
+
Run developer-style local improvement moves on the top solutions.
|
|
985
|
+
Phase determines which moves are used:
|
|
986
|
+
- early (first 40%): broad exploration (structural sweep, substitutions)
|
|
987
|
+
- mid (40-70%): refinement (significance pruning, distribution sweep)
|
|
988
|
+
- late (70-100%): convergence (model progression, ensemble crossover)
|
|
989
|
+
"""
|
|
990
|
+
if len(self.memory) < 2:
|
|
991
|
+
return
|
|
992
|
+
|
|
993
|
+
memory_sorted = self.sort_memory(self.memory[:])
|
|
994
|
+
top_n = min(5, len(memory_sorted))
|
|
995
|
+
candidates = memory_sorted[:top_n]
|
|
996
|
+
|
|
997
|
+
# Weight schedule: more moves in mid/late phases
|
|
998
|
+
if phase == 'early':
|
|
999
|
+
moves = [self.make_change_16_structural_hypothesis,
|
|
1000
|
+
self.make_change_15_feature_substitution]
|
|
1001
|
+
per_candidate = 1
|
|
1002
|
+
elif phase == 'mid':
|
|
1003
|
+
moves = [self.make_change_11_significance_prune,
|
|
1004
|
+
self.make_change_12_distribution_sweep,
|
|
1005
|
+
self.make_change_15_feature_substitution]
|
|
1006
|
+
per_candidate = 2
|
|
1007
|
+
else:
|
|
1008
|
+
moves = [self.make_change_13_model_progression,
|
|
1009
|
+
self.make_change_14_ensemble_crossover,
|
|
1010
|
+
self.make_change_11_significance_prune]
|
|
1011
|
+
per_candidate = 3
|
|
1012
|
+
|
|
1013
|
+
for cand in candidates[:per_candidate]:
|
|
1014
|
+
for move in moves:
|
|
1015
|
+
try:
|
|
1016
|
+
move(cand)
|
|
1017
|
+
except Exception:
|
|
1018
|
+
pass
|
|
1019
|
+
|
|
708
1020
|
def local_search(self):
|
|
709
1021
|
# {
|
|
710
|
-
#
|
|
711
|
-
|
|
1022
|
+
# ── Phase-aware local search ─────────────────────────────────
|
|
1023
|
+
# Uses the current improvisation progress to switch between
|
|
1024
|
+
# exploration (early), refinement (mid), and convergence (late).
|
|
1025
|
+
progress = getattr(self, '_improv_iter', 0) / max(self.maxiter, 1)
|
|
1026
|
+
if progress < 0.4:
|
|
1027
|
+
phase = 'early'
|
|
1028
|
+
elif progress < 0.7:
|
|
1029
|
+
phase = 'mid'
|
|
1030
|
+
else:
|
|
1031
|
+
phase = 'late'
|
|
1032
|
+
self.local_search_developer(0, phase=phase)
|
|
712
1033
|
|
|
713
|
-
#
|
|
1034
|
+
# Also run the original local search changes (1-10) at reduced rate
|
|
1035
|
+
candidate = [sol for sol in self.memory if abs(sol.obj[0]) < BOUND]
|
|
714
1036
|
# }
|
|
715
1037
|
|
|
716
1038
|
''' ---------------------------------------------------------- '''
|
|
@@ -755,6 +1077,8 @@ class HarmonySearch(Search):
|
|
|
755
1077
|
best, current = [], []
|
|
756
1078
|
for iter in range(self.maxiter):
|
|
757
1079
|
# {
|
|
1080
|
+
self._improv_iter = iter # track for phase-aware local search
|
|
1081
|
+
|
|
758
1082
|
# Compute consideration rate and pitch value
|
|
759
1083
|
# This code introduces oscillations (a.k.a., variations) based on the iteration number.
|
|
760
1084
|
# The result is scaled by the sine function only when its value is non-negative.
|
|
@@ -768,13 +1092,10 @@ class HarmonySearch(Search):
|
|
|
768
1092
|
# {
|
|
769
1093
|
self.insert_solution(curr_sol)
|
|
770
1094
|
|
|
771
|
-
#
|
|
772
|
-
#
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
#best.append(best_sol.obj[0])
|
|
776
|
-
#current.append(curr_sol.obj[0])
|
|
777
|
-
# }
|
|
1095
|
+
# Phase-aware local search: every 10 iterations, run
|
|
1096
|
+
# developer-style moves on the top solutions
|
|
1097
|
+
if iter > 0 and iter % 10 == 0 and len(self.memory) >= 2:
|
|
1098
|
+
self.local_search()
|
|
778
1099
|
# }
|
|
779
1100
|
# }
|
|
780
1101
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.0.129
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: SearchLibrium
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.129
|
|
4
4
|
Summary: A Python package for econometric models driven by search
|
|
5
5
|
Author: Mario Fernando Taco Morales, Alexander Paz, Prithvi Beeramole, Robert Burdett
|
|
6
6
|
Author-email: Zeke Ahern <z.ahern@qut.edu.au>
|
|
7
7
|
Project-URL: Homepage, https://pypi.org/project/SearchLibrium/
|
|
8
|
+
Project-URL: Repository, https://github.com/zahern/HypothesisX
|
|
9
|
+
Project-URL: Issues, https://github.com/zahern/HypothesisX/issues
|
|
10
|
+
Project-URL: Documentation, https://pypi.org/project/SearchLibrium/
|
|
8
11
|
Keywords: econometric models,search,discrete choice,logit,probit
|
|
9
12
|
Classifier: Development Status :: 4 - Beta
|
|
10
13
|
Classifier: Intended Audience :: Science/Research
|
|
@@ -41,8 +44,9 @@ Requires-Dist: pytest; extra == "dev"
|
|
|
41
44
|
[](https://pypi.org/project/SearchLibrium/)
|
|
42
45
|
[](LICENSE)
|
|
43
46
|
[](https://github.com/zahern/HypothesisX/actions/workflows/ci.yml)
|
|
47
|
+
[](https://github.com/zahern/HypothesisX)
|
|
44
48
|
|
|
45
|
-
**Automated discrete choice model search powered by Simulated Annealing, Harmony Search, and JAX-accelerated MLE.**
|
|
49
|
+
**Automated discrete choice model search powered by Simulated Annealing, Bandit-guided SA, Harmony Search, SA+PBIL, HS+PBIL, Parallel SA, and JAX-accelerated MLE.**
|
|
46
50
|
|
|
47
51
|
SearchLibrium searches over model specifications — which variables to include, whether parameters should be random, which transformations to apply, and which model class to use — and returns the best converged, all-significant model according to your chosen criterion (BIC, AIC, log-likelihood, MAE, or multi-objective combinations).
|
|
48
52
|
|
|
@@ -122,16 +126,19 @@ A **run dashboard** is printed automatically at the end of every search, showing
|
|
|
122
126
|
|
|
123
127
|
## How the search works
|
|
124
128
|
|
|
125
|
-
The search uses **
|
|
129
|
+
The search uses **metaheuristic optimisation** (SA, BanditSA, Harmony Search,
|
|
130
|
+
SA+PBIL, HS+PBIL, or parallel variants) to explore the space of model
|
|
131
|
+
specifications:
|
|
126
132
|
|
|
127
133
|
```text
|
|
128
134
|
generate starting solution
|
|
129
|
-
└─ for each
|
|
135
|
+
└─ for each iteration / temperature step / improvisation
|
|
130
136
|
└─ perturb current specification → guaranteed distinct from current
|
|
131
137
|
├─ fit model with JAX-accelerated MLE
|
|
132
138
|
├─ run backward elimination (remove insignificant vars, refit)
|
|
133
|
-
├─ accept if converged +
|
|
134
|
-
|
|
139
|
+
├─ accept if converged + acceptance criterion satisfied
|
|
140
|
+
│ (Metropolis, dominance, or pitch adjustment)
|
|
141
|
+
└─ update best solution / harmony memory
|
|
135
142
|
print dashboard
|
|
136
143
|
```
|
|
137
144
|
|
|
@@ -433,27 +440,42 @@ python -m SearchLibrium --test_search_nest # run nested logit search
|
|
|
433
440
|
|
|
434
441
|
## Search algorithms
|
|
435
442
|
|
|
436
|
-
|
|
443
|
+
All algorithms share a **consistent interface**. The library auto-estimates
|
|
444
|
+
hyperparameters from problem size when you omit `ctrl`:
|
|
437
445
|
|
|
438
446
|
```python
|
|
439
|
-
from SearchLibrium import call_search,
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
#
|
|
446
|
-
best = call_search(params,
|
|
447
|
-
best = call_search(params, algorithm='
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
# Inspect auto-estimated
|
|
447
|
+
from SearchLibrium import (call_search, call_siman, call_harmony, call_sapbil,
|
|
448
|
+
call_banditsa, call_harmony_pbil,
|
|
449
|
+
call_parsa, call_parcopsa, estimate_ctrl)
|
|
450
|
+
|
|
451
|
+
# Unified entry point — auto hyperparameters (recommended)
|
|
452
|
+
best = call_search(params) # SA (default)
|
|
453
|
+
best = call_search(params, algorithm='hs') # Harmony Search
|
|
454
|
+
best = call_search(params, algorithm='sapbil') # SA + PBIL
|
|
455
|
+
best = call_search(params, algorithm='banditsa') # Bandit-guided SA
|
|
456
|
+
best = call_search(params, algorithm='hspbil') # HS + PBIL
|
|
457
|
+
|
|
458
|
+
# Inspect auto-estimated hyperparameters before running
|
|
451
459
|
ctrl = estimate_ctrl(params, algorithm='sa')
|
|
452
460
|
print(ctrl)
|
|
453
461
|
```
|
|
454
462
|
|
|
463
|
+
### Algorithm reference
|
|
464
|
+
|
|
465
|
+
| Function | Description | Population-based | Key idea |
|
|
466
|
+
| -------- | ----------- | :---: | -------- |
|
|
467
|
+
| `call_siman` | Simulated Annealing | | Metropolis acceptance with temperature cooling |
|
|
468
|
+
| `call_banditsa` | Bandit-guided SA | | Thompson Sampling to choose perturbation actions |
|
|
469
|
+
| `call_sapbil` | SA + PBIL | ✓ | Probability matrix learns from accepted solutions to bias perturbations |
|
|
470
|
+
| `call_harmony` | Harmony Search | ✓ | Memory-based improvisation with pitch adjustment |
|
|
471
|
+
| `call_harmony_pbil` | HS + PBIL | ✓ | Harmony Search with PBIL-guided pitch perturbation |
|
|
472
|
+
| `call_parsa` | Parallel SA | | Multiple independent SA solvers running in parallel |
|
|
473
|
+
| `call_parcopsa` | Parallel Cooperative SA | ✓ | Parallel SA with periodic best-solution sharing |
|
|
474
|
+
|
|
455
475
|
### Simulated Annealing (`call_siman` / `algorithm='sa'`)
|
|
456
476
|
|
|
477
|
+
Control tuple: `(tI, tF, max_temp_steps, max_iter)`
|
|
478
|
+
|
|
457
479
|
| Parameter | Meaning |
|
|
458
480
|
| --------- | ------- |
|
|
459
481
|
| `tI` | Initial temperature — higher → more exploration |
|
|
@@ -467,12 +489,14 @@ best = call_siman(params, ctrl=(1000, 0.001, 100, 20), id_num=1)
|
|
|
467
489
|
|
|
468
490
|
### Harmony Search (`call_harmony` / `algorithm='hs'`)
|
|
469
491
|
|
|
492
|
+
Control tuple: `(max_mem, maxiter, max_harm, min_harm, max_pitch, min_pitch)`
|
|
493
|
+
|
|
470
494
|
| Parameter | Meaning |
|
|
471
495
|
| --------- | ------- |
|
|
472
496
|
| `max_mem` | Harmony memory size (population) |
|
|
473
497
|
| `maxiter` | Improvisation iterations |
|
|
474
|
-
| `max_harm` | Max harmony consideration rate |
|
|
475
|
-
| `min_harm` | Min harmony consideration rate |
|
|
498
|
+
| `max_harm` | Max harmony memory consideration rate |
|
|
499
|
+
| `min_harm` | Min harmony memory consideration rate |
|
|
476
500
|
| `max_pitch` | Max pitch adjustment rate |
|
|
477
501
|
| `min_pitch` | Min pitch adjustment rate |
|
|
478
502
|
|
|
@@ -480,6 +504,44 @@ best = call_siman(params, ctrl=(1000, 0.001, 100, 20), id_num=1)
|
|
|
480
504
|
best = call_harmony(params, ctrl=(20, 400, 0.9, 0.6, 0.85, 0.3), id_num=1)
|
|
481
505
|
```
|
|
482
506
|
|
|
507
|
+
### SA + PBIL (`call_sapbil` / `algorithm='sapbil'`)
|
|
508
|
+
|
|
509
|
+
Population-Based Incremental Learning coupled with Simulated Annealing.
|
|
510
|
+
Maintains a probability matrix over the search decisions (inclusion,
|
|
511
|
+
randomness, distribution, correlation, Box-Cox) and updates it from
|
|
512
|
+
accepted solutions using a temperature-coupled learning rate.
|
|
513
|
+
|
|
514
|
+
```python
|
|
515
|
+
best = call_sapbil(params, id_num=1)
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### HS + PBIL (`call_harmony_pbil` / `algorithm='hspbil'`)
|
|
519
|
+
|
|
520
|
+
Harmony Search with PBIL-guided pitch adjustment. Instead of uniform-random
|
|
521
|
+
perturbations during pitch adjustment, the PBIL probability matrix guides
|
|
522
|
+
add/remove decisions based on learned probabilities.
|
|
523
|
+
|
|
524
|
+
```python
|
|
525
|
+
best = call_harmony_pbil(params, id_num=1)
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### Bandit-guided SA (`call_banditsa` / `algorithm='banditsa'`)
|
|
529
|
+
|
|
530
|
+
Uses Thompson Sampling over a multi-armed bandit of perturbation actions
|
|
531
|
+
(add asvar, remove asvar, change distribution, ...). Actions that
|
|
532
|
+
historically led to accepted solutions are more likely to be chosen again.
|
|
533
|
+
|
|
534
|
+
```python
|
|
535
|
+
best = call_banditsa(params, id_num=1)
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
### Parallel SA variants
|
|
539
|
+
|
|
540
|
+
```python
|
|
541
|
+
best = call_parsa(params, nthrds=4) # 4 independent SA solvers
|
|
542
|
+
best = call_parcopsa(params, nthrds=8) # 8 SA solvers sharing best periodically
|
|
543
|
+
```
|
|
544
|
+
|
|
483
545
|
### Auto hyperparameter estimation
|
|
484
546
|
|
|
485
547
|
If `ctrl` is omitted, the library estimates appropriate defaults from the
|
|
@@ -489,8 +551,6 @@ problem complexity (`n_vars × n_alts × n_models`, doubled for random params):
|
|
|
489
551
|
from SearchLibrium import estimate_ctrl
|
|
490
552
|
ctrl_sa = estimate_ctrl(params, algorithm='sa')
|
|
491
553
|
ctrl_hs = estimate_ctrl(params, algorithm='hs')
|
|
492
|
-
print('SA ctrl:', ctrl_sa)
|
|
493
|
-
print('HS ctrl:', ctrl_hs)
|
|
494
554
|
```
|
|
495
555
|
|
|
496
556
|
Complexity buckets:
|
|
@@ -504,6 +564,64 @@ Complexity buckets:
|
|
|
504
564
|
|
|
505
565
|
|
|
506
566
|
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
## Constraints
|
|
570
|
+
|
|
571
|
+
Use `ConstraintBuilder` to enforce model structure rules during the search:
|
|
572
|
+
|
|
573
|
+
```python
|
|
574
|
+
from SearchLibrium import Parameters, ConstraintBuilder, call_siman
|
|
575
|
+
|
|
576
|
+
constraints = ConstraintBuilder()
|
|
577
|
+
|
|
578
|
+
# ── Inclusion / exclusion ──
|
|
579
|
+
constraints.force_include('TIME', 'COST') # must always appear
|
|
580
|
+
constraints.never_include('ID', 'DUMMY') # must never appear
|
|
581
|
+
|
|
582
|
+
# ── Mutually exclusive groups ──
|
|
583
|
+
# At most ONE variable from each group may appear in any solution.
|
|
584
|
+
# The search will never add a partner if a group member is already
|
|
585
|
+
# present, and will remove extras if they slip through.
|
|
586
|
+
constraints.mutually_exclusive('SPEED', 'TIME') # speed OR time, never both
|
|
587
|
+
constraints.mutually_exclusive('LOG_INCOME', 'INCOME_DUMMY') # independent group
|
|
588
|
+
|
|
589
|
+
# ── Minimum behavioural content ──
|
|
590
|
+
# Require at least N variables from a pool without locking in which ones.
|
|
591
|
+
constraints.min_behavioral(2, 'PRICE', 'BIKELANE', 'DIST6', 'RECRE')
|
|
592
|
+
|
|
593
|
+
# ── Random-parameter rules ──
|
|
594
|
+
constraints.force_random('TIME', distribution='n') # must have random parameter
|
|
595
|
+
constraints.exclude_random('HEADWAY') # must never be random
|
|
596
|
+
|
|
597
|
+
# ── Attach constraints to the Parameters object ──
|
|
598
|
+
params = Parameters(
|
|
599
|
+
criterions = [("bic", -1)],
|
|
600
|
+
df = df,
|
|
601
|
+
varnames = varnames,
|
|
602
|
+
asvarnames = varnames,
|
|
603
|
+
choice_set = choice_set,
|
|
604
|
+
choices = df["CHOICE"].values,
|
|
605
|
+
alt_var = df["alt"].values,
|
|
606
|
+
choice_id = df["custom_id"].values,
|
|
607
|
+
ind_id = df["ID"].values,
|
|
608
|
+
base_alt = "SM",
|
|
609
|
+
models = ["multinomial", "mixed_logit"],
|
|
610
|
+
allow_random = True,
|
|
611
|
+
p_val = 0.05,
|
|
612
|
+
pre_spec_constraints = constraints.dict(), # ◄ attach here
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
best = call_siman(params, id_num=1)
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
The constraint enforcement happens at every stage:
|
|
619
|
+
- **Solution generation** — filtered so violations are never created
|
|
620
|
+
- **Perturbation / mutation** — blocked from adding violating variables
|
|
621
|
+
- **Post-evaluation cleanup** — extra members are removed if somehow present
|
|
622
|
+
|
|
623
|
+
---
|
|
624
|
+
|
|
507
625
|
## License
|
|
508
626
|
|
|
509
627
|
MIT — see [LICENSE](LICENSE) for details.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.0.127
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium/test_sapbil_vs_banditsa.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{searchlibrium-0.0.127 → searchlibrium-0.0.129}/src/SearchLibrium.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|