SearchLibrium 0.0.119__tar.gz → 0.0.121__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.
Files changed (52) hide show
  1. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/PKG-INFO +1 -1
  2. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/pyproject.toml +1 -1
  3. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/MixedLogit.py +4 -1
  4. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/__init__.py +4 -2
  5. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/call_meta.py +53 -1
  6. searchlibrium-0.0.121/src/SearchLibrium/hspbil.py +299 -0
  7. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/latent_class.py +233 -26
  8. searchlibrium-0.0.121/src/SearchLibrium/version.txt +1 -0
  9. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium.egg-info/PKG-INFO +1 -1
  10. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium.egg-info/SOURCES.txt +1 -0
  11. searchlibrium-0.0.119/src/SearchLibrium/version.txt +0 -1
  12. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/README.md +0 -0
  13. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/setup.cfg +0 -0
  14. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/Halton.py +0 -0
  15. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/Mode_Activity_Nested.py +0 -0
  16. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/RandomP.py +0 -0
  17. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/SEARCH_SM_MARIO.py +0 -0
  18. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/Two_Level_Nest.py +0 -0
  19. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/__main__.py +0 -0
  20. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/_choice_model.py +0 -0
  21. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/_device.py +0 -0
  22. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/banditsa.py +0 -0
  23. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/bhhh/minimize.py +0 -0
  24. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/boxcox_functions.py +0 -0
  25. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/constraints_builder.py +0 -0
  26. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/harmony.py +0 -0
  27. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/main.py +0 -0
  28. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/main_debug.py +0 -0
  29. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/mdcev.py +0 -0
  30. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/misc.py +0 -0
  31. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/mixed_logit.py +0 -0
  32. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/mixed_nested.py +0 -0
  33. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/mixedrrm.py +0 -0
  34. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/multinomial_logit.py +0 -0
  35. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/multinomial_nested.py +0 -0
  36. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/multinomial_probit.py +0 -0
  37. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/ordered_logit.py +0 -0
  38. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/ordered_logit_mixed.py +0 -0
  39. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/rrm.py +0 -0
  40. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/sapbil.py +0 -0
  41. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/search.py +0 -0
  42. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/selection_models.py +0 -0
  43. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/setup.py +0 -0
  44. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/siman.py +0 -0
  45. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/test_lc_de.py +0 -0
  46. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/test_mario_searches.py +0 -0
  47. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/test_sapbil_vs_banditsa.py +0 -0
  48. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium/threshold.py +0 -0
  49. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium.egg-info/dependency_links.txt +0 -0
  50. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium.egg-info/entry_points.txt +0 -0
  51. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium.egg-info/requires.txt +0 -0
  52. {searchlibrium-0.0.119 → searchlibrium-0.0.121}/src/SearchLibrium.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SearchLibrium
3
- Version: 0.0.119
3
+ Version: 0.0.121
4
4
  Summary: A Python package for econometric models driven by search
5
5
  Author: Alexander Paz Prithvi Beeramole, Robert Burdett
6
6
  Author-email: Zeke Ahern <z.ahern@qut.edu.au>
@@ -68,7 +68,7 @@ Homepage = "https://github.com/zahern/HypothesisX"
68
68
  realpython = "SearchLibrium.__main__:main"
69
69
 
70
70
  [tool.bumpver]
71
- current_version = "0.0.119"
71
+ current_version = "0.0.121"
72
72
  version_pattern = "MAJOR.MINOR.PATCH"
73
73
  commit_message = "[skip ci] Bump version {old_version} -> {new_version}"
74
74
  commit = true
@@ -140,7 +140,7 @@ class MixedLogit(DiscreteChoiceModel):
140
140
  gtol=1e-6, return_hess=True, return_grad=True, method="slsqp",
141
141
  save_fitted_params=True, mnl_init=True,
142
142
  de_init=False, de_popsize=4, de_maxiter=3, de_tol=0.5,
143
- de_polish=False):
143
+ de_polish=False, l1_penalty=0.0):
144
144
  # {
145
145
  self.fit_intercept = fit_intercept
146
146
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -185,6 +185,7 @@ class MixedLogit(DiscreteChoiceModel):
185
185
  self.de_polish = de_polish
186
186
  self.total_fun_eval = 0
187
187
  self.method = method.lower() if hasattr(method, 'lower') else method
188
+ self.l1_penalty = float(l1_penalty)
188
189
  self.jac = self.return_grad # scipy optimize parameter
189
190
  self.n_draws = n_draws
190
191
  self.batch_size = min(n_draws, batch_size) if batch_size is not None else n_draws
@@ -1033,6 +1034,7 @@ class MixedLogit(DiscreteChoiceModel):
1033
1034
 
1034
1035
  penalty = self.regularize_loglik(betas)
1035
1036
  loglik = loglik - penalty
1037
+ loglik = loglik - self.regularize_l1_loglik(betas)
1036
1038
 
1037
1039
  self.total_fun_eval += 1
1038
1040
 
@@ -1054,6 +1056,7 @@ class MixedLogit(DiscreteChoiceModel):
1054
1056
  # }
1055
1057
 
1056
1058
  g = np.sum(g, axis=0) / n_batches # (K, )
1059
+ g = g - self.regularize_l1_grad(betas)
1057
1060
  self.gtol_res = np.linalg.norm(g, ord=np.inf)
1058
1061
 
1059
1062
  result = (-loglik,) # Create a tuple
@@ -111,7 +111,8 @@ try:
111
111
 
112
112
  from . import misc
113
113
  from .sapbil import SAPBIL, ProbabilityMatrix
114
- from .call_meta import call_harmony, call_siman, call_parsa, call_search, call_sapbil, estimate_ctrl
114
+ from .hspbil import HSPBIL
115
+ from .call_meta import call_harmony, call_harmony_pbil, call_siman, call_parsa, call_search, call_sapbil, estimate_ctrl
115
116
 
116
117
  except ImportError as e:
117
118
  from _choice_model import DiscreteChoiceModel
@@ -130,7 +131,8 @@ except ImportError as e:
130
131
  from constraints_builder import ConstraintBuilder, create_constraints
131
132
  from search import Parameters
132
133
  from sapbil import SAPBIL, ProbabilityMatrix
133
- from call_meta import call_siman, call_harmony, call_search, call_sapbil, estimate_ctrl
134
+ from hspbil import HSPBIL
135
+ from call_meta import call_siman, call_harmony, call_harmony_pbil, call_search, call_sapbil, estimate_ctrl
134
136
  try:
135
137
  from .main import print_ascii_art_logo
136
138
  except Exception:
@@ -24,12 +24,14 @@ try:
24
24
  from banditsa import*
25
25
  from threshold import*
26
26
  from sapbil import SAPBIL, ProbabilityMatrix
27
+ from hspbil import HSPBIL
27
28
  except ImportError:
28
29
  from .harmony import*
29
30
  from .siman import*
30
31
  from .banditsa import*
31
32
  from .threshold import*
32
33
  from .sapbil import SAPBIL, ProbabilityMatrix
34
+ from .hspbil import HSPBIL
33
35
 
34
36
  import numpy as np
35
37
 
@@ -447,6 +449,54 @@ def call_harmony(parameters, init_sol=None, ctrl=None, **kwargs):
447
449
  return best
448
450
 
449
451
 
452
+ # ─────────────────────────────────────────────────────────────────────────────
453
+ # Harmony Search + PBIL
454
+ # ─────────────────────────────────────────────────────────────────────────────
455
+
456
+ def call_harmony_pbil(parameters, init_sol=None, ctrl=None, **kwargs):
457
+ """Run Harmony Search with PBIL-guided pitch adjustment.
458
+
459
+ Parameters
460
+ ----------
461
+ parameters : Parameters
462
+ Problem definition.
463
+ init_sol : Solution, optional
464
+ Warm-start solution (passed as existing memory).
465
+ ctrl : tuple, optional
466
+ ``(max_mem, maxiter, max_harm, min_harm, max_pitch, min_pitch)``.
467
+ If omitted the values are estimated from the problem size.
468
+ **kwargs
469
+ ``id_num`` — run identifier.
470
+
471
+ Returns
472
+ -------
473
+ Solution
474
+ Best solution in the final harmony memory.
475
+ """
476
+ if ctrl is None:
477
+ ctrl = kwargs.pop('ctrl', None)
478
+
479
+ id_num = kwargs.pop('id_num', None)
480
+
481
+ if ctrl is None:
482
+ ctrl = estimate_ctrl(parameters, algorithm='hs')
483
+ print(f"[HS+PBIL] Auto-estimated hyperparameters (problem complexity "
484
+ f"= {_problem_size(parameters)['complexity']}):")
485
+ else:
486
+ print(f"[HS+PBIL] Using provided hyperparameters:")
487
+
488
+ print(_describe_ctrl(ctrl, 'hs'))
489
+ print()
490
+
491
+ solver = HSPBIL(parameters, init_sol, ctrl, idnum=id_num, **kwargs)
492
+ existing = [init_sol] if init_sol is not None else None
493
+ solver.run_search(existing_sols=existing)
494
+ solver.close_files()
495
+ best = solver.return_best()
496
+ _print_dashboard(solver, best, algorithm='HS+PBIL')
497
+ return best
498
+
499
+
450
500
  # ─────────────────────────────────────────────────────────────────────────────
451
501
  # Unified entry point
452
502
  # ─────────────────────────────────────────────────────────────────────────────
@@ -496,10 +546,12 @@ def call_search(parameters, init_sol=None, algorithm='sa', ctrl=None, **kwargs):
496
546
  return call_banditsa(parameters, init_sol=init_sol, ctrl=ctrl, **kwargs)
497
547
  elif algorithm in ('hs', 'harmony', 'harmony_search'):
498
548
  return call_harmony(parameters, init_sol=init_sol, ctrl=ctrl, **kwargs)
549
+ elif algorithm in ('hspbil', 'harmony_pbil', 'hs_pbil', 'hs+pbil'):
550
+ return call_harmony_pbil(parameters, init_sol=init_sol, ctrl=ctrl, **kwargs)
499
551
  else:
500
552
  raise ValueError(
501
553
  f"Unknown algorithm '{algorithm}'. "
502
- f"Choose 'sa', 'sapbil', 'banditsa', or 'hs'."
554
+ f"Choose 'sa', 'sapbil', 'banditsa', 'hs', or 'hspbil'."
503
555
  )
504
556
 
505
557
 
@@ -0,0 +1,299 @@
1
+ """HS+PBIL: Harmony Search coupled with Population-Based Incremental Learning.
2
+
3
+ Reference:
4
+ Taco-Morales, M.F. (2026). SA + PBIL: Coupling Simulated Annealing with
5
+ Population-Based Incremental Learning for Discrete Choice Model Specification
6
+ Search. QUT Master's Thesis, May 2026.
7
+
8
+ This module extends the PBIL probability-guided perturbation mechanism from
9
+ SAPBIL to the Harmony Search algorithm. Instead of uniform-random
10
+ perturbations during pitch adjustment, the PBIL probability matrix guides
11
+ add/remove decisions for variables, increasing the probability of choosing
12
+ directions that led to significant parameters in previously accepted solutions.
13
+ """
14
+
15
+ import copy
16
+ import logging
17
+ import numpy as np
18
+ import time as _time
19
+
20
+ try:
21
+ from .harmony import HarmonySearch
22
+ from .sapbil import ProbabilityMatrix
23
+ except ImportError:
24
+ from harmony import HarmonySearch
25
+ from sapbil import ProbabilityMatrix
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ # pylint: disable=too-many-ancestors,attribute-defined-outside-init
31
+ class HSPBIL(HarmonySearch):
32
+ """Harmony Search with PBIL-guided pitch adjustment.
33
+
34
+ Overrides the standard uniform-random pitch perturbations with
35
+ probability-weighted decisions that learn from accepted solutions.
36
+ """
37
+
38
+ def __init__(self, param, init_sol, ctrl, idnum=0, **kwargs):
39
+ super().__init__(param, init_sol, ctrl, idnum=idnum, **kwargs)
40
+
41
+ varnames = list(param.asvarnames or [])
42
+ distributions = list(param.distr or ["n", "ln", "tn", "u", "t"])
43
+ self.prob_matrix = ProbabilityMatrix(varnames, distributions)
44
+
45
+ self._ps_asvars = set(getattr(param, "ps_asvars", []) or [])
46
+ self._ps_randvars = set(
47
+ (getattr(param, "ps_randvars", {}) or {}).keys()
48
+ )
49
+ self._ps_bcvars = set(getattr(param, "ps_bcvars", []) or [])
50
+ self._ps_corvars = set(getattr(param, "ps_corvars", []) or [])
51
+
52
+ self._pbil_updates = 0
53
+
54
+ # ------------------------------------------------------------------
55
+ # Output files (override to use "hspbil_" prefix)
56
+ # ------------------------------------------------------------------
57
+
58
+ def create_output_files(self, param, **kwargs):
59
+ run_id = kwargs.get('idnum', 0)
60
+ ts = _time.strftime("%Y%m%d_%H%M%S")
61
+ run_name = f"hspbil_{run_id}_{ts}" if run_id else f"hspbil_{ts}"
62
+
63
+ try:
64
+ self.results_file = open(f"{run_name}_results.txt", "w")
65
+ except Exception:
66
+ self.results_file = open(f"hspbil_results.txt", "w")
67
+
68
+ try:
69
+ self.progress_file = open(f"{run_name}_progress.txt", "w")
70
+ except Exception:
71
+ self.progress_file = open(f"hspbil_progress.txt", "w")
72
+
73
+ print("SearchLibrium - HS+PBIL Run", file=self.results_file)
74
+ print(f"Run ID: {run_id}", file=self.results_file)
75
+ print(f"Start time: {_time.strftime('%Y-%m-%d %H:%M:%S')}", file=self.results_file)
76
+ print("-" * 72, file=self.results_file)
77
+ self.results_file.flush()
78
+
79
+ print("iteration,score", file=self.progress_file)
80
+ self.progress_file.flush()
81
+
82
+ # ------------------------------------------------------------------
83
+ # PBIL significance helpers (same as SAPBIL)
84
+ # ------------------------------------------------------------------
85
+
86
+ def _build_significance_map(self, sol):
87
+ model = sol.get("model")
88
+ if model is None:
89
+ return {}
90
+ try:
91
+ pvalues = np.array(model.pvalues)
92
+ coeff_names = list(model.coeff_names) if model.coeff_names is not None else []
93
+ p_thresh = getattr(self.param, "p_val", 0.05)
94
+ return {
95
+ name: bool(pv <= p_thresh)
96
+ for name, pv in zip(coeff_names, pvalues)
97
+ }
98
+ except Exception as exc:
99
+ logger.debug("HS+PBIL: could not build significance map: %s", exc)
100
+ return {}
101
+
102
+ def _update_probability_matrix(self, sol):
103
+ pm = self.prob_matrix
104
+
105
+ sig = self._build_significance_map(sol)
106
+
107
+ asvars = set(sol.get("asvars", []) or [])
108
+ randvars = dict(sol.get("randvars", {}) or {})
109
+ bcvars = set(sol.get("bcvars", []) or [])
110
+ corvars = set(sol.get("corvars", []) or [])
111
+
112
+ t = 1.0
113
+ tI = 1.0
114
+
115
+ for var in pm.varnames:
116
+ if var in self._ps_asvars:
117
+ continue
118
+
119
+ in_model = var in asvars
120
+ is_random = var in randvars
121
+ is_bc = var in bcvars
122
+ is_corr = var in corvars
123
+
124
+ if in_model:
125
+ mean_sig = sig.get(var, False)
126
+ sd_sig = sig.get(f"sd.{var}", False)
127
+ if mean_sig or (is_random and sd_sig):
128
+ incl_ind = 1.0
129
+ elif (not mean_sig) and (not is_random or (not mean_sig and not sd_sig)):
130
+ incl_ind = 0.0
131
+ else:
132
+ incl_ind = 0.5
133
+ else:
134
+ incl_ind = 0.0
135
+
136
+ pm.update_inclusion(var, incl_ind, t, tI)
137
+
138
+ if not in_model:
139
+ continue
140
+
141
+ if var not in self._ps_randvars:
142
+ if is_random:
143
+ rand_ind = 1.0 if sig.get(f"sd.{var}", False) else 0.0
144
+ else:
145
+ rand_ind = 0.0
146
+ pm.update_random(var, rand_ind, t, tI)
147
+
148
+ if not is_random:
149
+ continue
150
+
151
+ if var not in self._ps_randvars:
152
+ current_distr = randvars[var]
153
+ mean_sig_d = sig.get(var, False)
154
+ sd_sig_d = sig.get(f"sd.{var}", False)
155
+ distr_ind = 1.0 if (mean_sig_d and sd_sig_d) else 0.0
156
+ pm.update_distribution(var, current_distr, distr_ind, t, tI)
157
+
158
+ if not is_bc and var not in self._ps_corvars:
159
+ if is_corr:
160
+ chol_sig = any(
161
+ (
162
+ sig.get(f"chol.{var}.{v2}", False)
163
+ or sig.get(f"chol.{v2}.{var}", False)
164
+ )
165
+ for v2 in corvars
166
+ if v2 != var
167
+ )
168
+ corr_ind = 1.0 if chol_sig else 0.0
169
+ else:
170
+ corr_ind = 0.0
171
+ pm.update_correlation(var, corr_ind, t, tI)
172
+
173
+ if not is_corr and var not in self._ps_bcvars:
174
+ if is_bc:
175
+ bc_ind = 1.0 if sig.get(f"lambda.{var}", False) else 0.0
176
+ else:
177
+ bc_ind = 0.0
178
+ pm.update_boxcox(var, bc_ind, t, tI)
179
+
180
+ self._pbil_updates += 1
181
+
182
+ # ------------------------------------------------------------------
183
+ # PBIL-guided inclusion perturbation
184
+ # ------------------------------------------------------------------
185
+
186
+ def _pbil_inclusion(self, sol):
187
+ """Apply one PBIL-guided inclusion add/remove step."""
188
+ pm = self.prob_matrix
189
+ in_vars = [v for v in sol.get("asvars", []) if v in pm.p_inclusion]
190
+ out_vars = [v for v in pm.varnames if v not in sol.get("asvars", [])]
191
+
192
+ p_bar_in = float(np.mean([pm.p_inclusion[v] for v in in_vars])) if in_vars else 0.5
193
+ p_bar_out = float(np.mean([pm.p_inclusion[v] for v in out_vars])) if out_vars else 0.5
194
+
195
+ denom = p_bar_out + (1.0 - p_bar_in)
196
+ p_add = p_bar_out / denom if denom > 1e-12 else 0.5
197
+
198
+ if np.random.random() < p_add and out_vars:
199
+ probs = np.array([pm.p_inclusion[v] for v in out_vars], dtype=float)
200
+ probs = probs / (probs.sum() + 1e-12)
201
+ var = np.random.choice(out_vars, p=probs)
202
+ return self.add_asvar(var, sol)
203
+ elif in_vars:
204
+ non_ps = [v for v in in_vars if v not in self._ps_asvars]
205
+ if not non_ps or len(in_vars) <= 1:
206
+ return sol
207
+ probs = np.array([1.0 - pm.p_inclusion[v] for v in non_ps], dtype=float)
208
+ if probs.sum() < 1e-12:
209
+ probs = np.ones(len(non_ps), dtype=float)
210
+ probs = probs / probs.sum()
211
+ var = np.random.choice(non_ps, p=probs)
212
+ return self.remove_asvar(var, sol)
213
+ return sol
214
+
215
+ # ------------------------------------------------------------------
216
+ # PBIL override of key perturbation methods from SearchBase
217
+ # ------------------------------------------------------------------
218
+
219
+ def perturb_asfeature(self, sol):
220
+ if sol['asvars'] is None or len(sol['asvars']) == 0:
221
+ return self.perturb_add_asfeature(sol)
222
+ if np.random.random() < 0.5:
223
+ return self._pbil_inclusion(sol)
224
+ return super().perturb_asfeature(sol)
225
+
226
+ def perturb_isfeature(self, sol):
227
+ return super().perturb_isfeature(sol)
228
+
229
+ # ------------------------------------------------------------------
230
+ # Override improvise to track iteration counter for PBIL logging
231
+ # ------------------------------------------------------------------
232
+
233
+ def improvise(self):
234
+ best, current = [], []
235
+ for iter in range(self.maxiter):
236
+ sine_iter = max(0, np.sign(math.sin(iter)))
237
+ self.harm_rate = (self.min_harm + ((self.max_harm - self.min_harm) / self.maxiter) * iter) * sine_iter
238
+ self.pitch = (self.min_pitch + ((self.max_pitch - self.min_pitch) / self.maxiter) * iter) * sine_iter
239
+
240
+ new_sol = self.build_solution(self.memory, self.harm_rate)
241
+ curr_sol, converged = self.pitch_adjustment(new_sol, self.pitch)
242
+ if converged:
243
+ self.insert_solution(curr_sol)
244
+ try:
245
+ self._update_probability_matrix(curr_sol)
246
+ except Exception:
247
+ pass
248
+
249
+ all_val, obj_val = self.log_convergence(self.memory)
250
+ if self.generate_plots:
251
+ self.plot_results(self.memory, all_val, obj_val)
252
+
253
+ def run_search(self, existing_sols=None):
254
+ import time as _time
255
+
256
+ self.start = _time.time()
257
+
258
+ from .search import get_unique as _get_unique
259
+ try:
260
+ from .search import get_unique
261
+ except ImportError:
262
+ from search import get_unique
263
+
264
+ self.create_output_files(self.param, idnum=getattr(self, 'idnum', 0))
265
+
266
+ existing_memory = self.screen_solutions(existing_sols)
267
+ generated_memory = self.initialize_memory(self.max_mem)
268
+ init_memory = generated_memory + existing_memory
269
+ unique_memory = get_unique(init_memory, 0)
270
+ for sol in unique_memory:
271
+ sol.data['is_initial_sol'] = True
272
+
273
+ memory_sorted = self.sort_memory(unique_memory)
274
+ memory = memory_sorted[: self.max_mem]
275
+ self.memory = memory.copy()
276
+
277
+ if self.memory:
278
+ self.best_sol = self.copy_solution(self.sort_memory(self.memory)[0])
279
+
280
+ print(f"HS+PBIL[{getattr(self, 'idnum', 0)}] Memory initialised. "
281
+ f"Starting improvisation ({self.maxiter} iterations) ...")
282
+ self.improvise()
283
+
284
+ improved = self.sort_memory(self.memory.copy())
285
+ if improved:
286
+ self.best_sol = self.copy_solution(improved[0])
287
+
288
+ print(f"HS+PBIL[{getattr(self, 'idnum', 0)}]. Search complete")
289
+ logger.info("Search ended at: {}".format(str(_time.ctime())))
290
+
291
+ try:
292
+ pm_summary = self.prob_matrix.summary()
293
+ print("\nHS+PBIL -- final probability matrix:", file=self.results_file)
294
+ for row in pm_summary:
295
+ print(f" {row}", file=self.results_file)
296
+ except Exception:
297
+ pass
298
+
299
+ return improved
@@ -41,6 +41,8 @@ class LatentClassMixedLogit:
41
41
  n_init=1,
42
42
  optimise_membership=True,
43
43
  membership_maxiter=50,
44
+ l1_penalty=0.0,
45
+ l2_penalty=0.0,
44
46
  ):
45
47
  self.n_classes = int(n_classes)
46
48
  self.maxiter = int(maxiter)
@@ -50,6 +52,8 @@ class LatentClassMixedLogit:
50
52
  self.n_init = max(1, int(n_init))
51
53
  self.optimise_membership = bool(optimise_membership)
52
54
  self.membership_maxiter = int(membership_maxiter)
55
+ self.l1_penalty = float(l1_penalty)
56
+ self.l2_penalty = float(l2_penalty)
53
57
  self.descr = "LC-MXL"
54
58
  self.coeff_est = None
55
59
  self.coeff_names = None
@@ -87,7 +91,7 @@ class LatentClassMixedLogit:
87
91
 
88
92
  def setup(self, X, y, varnames, ids, alts, avail=None, fit_intercept=False,
89
93
  membership_vars=None, member_params_spec=None,
90
- class_params_spec=None):
94
+ class_params_spec=None, l1_penalty=None, l2_penalty=None):
91
95
  X = np.asarray(X, dtype=float)
92
96
  y = np.asarray(y, dtype=float)
93
97
  ids = np.asarray(ids)
@@ -108,6 +112,10 @@ class LatentClassMixedLogit:
108
112
  self.J = len(self.alts)
109
113
  self.K = X.shape[1]
110
114
  self.varnames = varnames
115
+ if l1_penalty is not None:
116
+ self.l1_penalty = float(l1_penalty)
117
+ if l2_penalty is not None:
118
+ self.l2_penalty = float(l2_penalty)
111
119
 
112
120
  order = np.lexsort((alts, ids))
113
121
  X = X[order]
@@ -129,6 +137,23 @@ class LatentClassMixedLogit:
129
137
  # ── Class-specific specification ────────────────────────────────────
130
138
  self._class_specs = None
131
139
  self._Ks = None # K per class
140
+
141
+ _member_util_exclude = set()
142
+ if membership_vars is not None:
143
+ for v in membership_vars:
144
+ if v in varnames:
145
+ _member_util_exclude.add(varnames.index(v))
146
+ elif member_params_spec is not None:
147
+ if hasattr(member_params_spec, 'shape'):
148
+ raw = member_params_spec.ravel() if member_params_spec.ndim > 1 else member_params_spec
149
+ for v in raw:
150
+ if isinstance(v, str) and v != '_inter' and v in varnames:
151
+ _member_util_exclude.add(varnames.index(v))
152
+ else:
153
+ for v in member_params_spec:
154
+ if isinstance(v, str) and v != '_inter' and v in varnames:
155
+ _member_util_exclude.add(varnames.index(v))
156
+
132
157
  if class_params_spec is not None:
133
158
  self._class_specs = []
134
159
  self._Ks = []
@@ -149,9 +174,15 @@ class LatentClassMixedLogit:
149
174
  self._Ks = np.array(self._Ks, dtype=int)
150
175
  self.K_tot = int(self._Ks.sum())
151
176
  else:
152
- self._Ks = np.full(self.n_classes, self.K, dtype=int)
153
- self._class_specs = [np.arange(self.K, dtype=int)] * self.n_classes
154
- self.K_tot = self.n_classes * self.K
177
+ if _member_util_exclude:
178
+ utility_idx = [i for i in range(self.K) if i not in _member_util_exclude]
179
+ self._Ks = np.full(self.n_classes, len(utility_idx), dtype=int)
180
+ self._class_specs = [np.array(utility_idx, dtype=int)] * self.n_classes
181
+ self.K_tot = self.n_classes * len(utility_idx)
182
+ else:
183
+ self._Ks = np.full(self.n_classes, self.K, dtype=int)
184
+ self._class_specs = [np.arange(self.K, dtype=int)] * self.n_classes
185
+ self.K_tot = self.n_classes * self.K
155
186
 
156
187
  # ── Membership data ────────────────────────────────────────────────
157
188
  self._has_membership = False
@@ -219,6 +250,29 @@ class LatentClassMixedLogit:
219
250
  else:
220
251
  self.X_memb_backend = None
221
252
 
253
+ def _regularize_l2_betas(self, betas):
254
+ if isinstance(betas, list):
255
+ return self.l2_penalty * sum(float(np.sum(np.square(np.asarray(b)))) for b in betas)
256
+ return self.l2_penalty * float(np.sum(np.square(np.asarray(betas))))
257
+
258
+ def _regularize_l1_betas(self, betas):
259
+ if isinstance(betas, list):
260
+ return self.l1_penalty * sum(float(np.sum(np.abs(np.asarray(b)))) for b in betas)
261
+ return self.l1_penalty * float(np.sum(np.abs(np.asarray(betas))))
262
+
263
+ def _regularize_l2_gammas(self, gammas):
264
+ if gammas is None or self.l2_penalty == 0:
265
+ return 0.0
266
+ return self.l2_penalty * float(np.sum(np.square(np.asarray(gammas))))
267
+
268
+ def _regularize_l1_gammas(self, gammas):
269
+ if gammas is None or self.l1_penalty == 0:
270
+ return 0.0
271
+ return self.l1_penalty * float(np.sum(np.abs(np.asarray(gammas))))
272
+
273
+ def _regularize_l1_grad(self, beta):
274
+ return self.l1_penalty * np.sign(np.asarray(beta, dtype=float))
275
+
222
276
  @staticmethod
223
277
  def _normalize_class_probs(class_probs):
224
278
  class_probs = np.clip(np.asarray(class_probs, dtype=float), 1e-12, None)
@@ -271,8 +325,8 @@ class LatentClassMixedLogit:
271
325
  """
272
326
  C = self.n_classes
273
327
  Km = self.K_membership
274
- gammas = gammas.reshape(C - 1, Km)
275
- priors = self._compute_membership_priors(gammas)
328
+ gammas_arr = gammas.reshape(C - 1, Km)
329
+ priors = self._compute_membership_priors(gammas_arr)
276
330
  log_priors = np.log(np.clip(priors, 1e-300, None))
277
331
  ll = np.sum(weights * log_priors)
278
332
 
@@ -281,7 +335,22 @@ class LatentClassMixedLogit:
281
335
  for c in range(C - 1):
282
336
  res_c = residuals[:, c]
283
337
  grad[c] = self.X_membership.T @ res_c
284
- return -ll, -grad.flatten()
338
+
339
+ l2 = self.l2_penalty
340
+ l1 = self.l1_penalty
341
+ if l2 > 0:
342
+ ll -= l2 * np.sum(np.square(gammas_arr))
343
+ if l1 > 0:
344
+ ll -= l1 * np.sum(np.abs(gammas_arr))
345
+
346
+ neg_obj = -ll
347
+ neg_grad = -grad
348
+ if l2 > 0:
349
+ neg_grad += 2.0 * l2 * gammas_arr
350
+ if l1 > 0:
351
+ neg_grad += l1 * np.sign(gammas_arr)
352
+
353
+ return neg_obj, neg_grad.flatten()
285
354
 
286
355
  def _membership_m_step(self, gamma0, weights):
287
356
  """M-step for membership (gamma) parameters.
@@ -361,6 +430,84 @@ class LatentClassMixedLogit:
361
430
  chosen_prob = np.clip((probs * self.y[:, None, :]).sum(axis=2), 1e-300, None)
362
431
  return np.log(chosen_prob), probs
363
432
 
433
+ def _build_jax_full_objective(self):
434
+ cache_key = "_jax_full_obj"
435
+ if hasattr(self, cache_key):
436
+ return getattr(self, cache_key)
437
+
438
+ if not self._jax_enabled or len(set(self._Ks)) != 1:
439
+ setattr(self, cache_key, None)
440
+ return None
441
+
442
+ jnp = self.jnp
443
+ C = self.n_classes
444
+ K = int(self._Ks[0])
445
+ n_phi = C - 1
446
+ avail = self.avail_backend
447
+ y_b = self.y_backend
448
+ X_full = self.X_backend
449
+ util_spec = self._class_specs[0] if self._class_specs is not None else None
450
+ if util_spec is not None and len(util_spec) < X_full.shape[2]:
451
+ X_b = X_full[:, :, util_spec]
452
+ else:
453
+ X_b = X_full
454
+ N = int(X_b.shape[0])
455
+ has_memb = bool(self._has_membership and self.optimise_membership
456
+ and self.K_membership > 0)
457
+ Km = self.K_membership if has_memb else 0
458
+ X_memb = self.X_memb_backend if has_memb else None
459
+ l2 = self.l2_penalty
460
+ l1 = self.l1_penalty
461
+
462
+ def _negloglik_flat(params):
463
+ phi = params[:n_phi]
464
+ beta_flat = params[n_phi:n_phi + C * K]
465
+ betas = beta_flat.reshape(C, K)
466
+
467
+ utilities = jnp.einsum("ck,njk->ncj", betas, X_b)
468
+ utilities = jnp.where(avail[:, None, :] > 0, utilities, -1e10)
469
+ utilities = utilities - jnp.max(utilities, axis=2, keepdims=True)
470
+ exp_u = jnp.exp(utilities) * avail[:, None, :]
471
+ denom = jnp.clip(exp_u.sum(axis=2, keepdims=True), 1e-300)
472
+ probs = exp_u / denom
473
+ chosen = jnp.clip((probs * y_b[:, None, :]).sum(axis=2), 1e-300)
474
+ log_chosen = jnp.log(chosen)
475
+
476
+ if has_memb:
477
+ gammas = params[n_phi + C * K:].reshape(C - 1, Km)
478
+ logits = jnp.zeros((N, C))
479
+ for c in range(C - 1):
480
+ logits = logits.at[:, c].set(X_memb @ gammas[c])
481
+ logits = logits - jnp.max(logits, axis=1, keepdims=True)
482
+ exp_logits = jnp.exp(logits)
483
+ priors = exp_logits / jnp.clip(jnp.sum(exp_logits, axis=1, keepdims=True), 1e-300, None)
484
+ log_prior = jnp.log(jnp.clip(priors, 1e-300))
485
+ else:
486
+ phi_full = jnp.concatenate([phi, jnp.zeros(1)])
487
+ log_priors_raw = phi_full - self.jax_logsumexp(phi_full)
488
+ log_prior = jnp.broadcast_to(log_priors_raw[None, :], (N, C))
489
+
490
+ log_joint = log_chosen + log_prior
491
+ log_marg = self.jax_logsumexp(log_joint, axis=1)
492
+ ll = jnp.sum(log_marg)
493
+ ll -= l2 * jnp.sum(jnp.square(betas))
494
+ ll -= l1 * jnp.sum(jnp.abs(betas))
495
+ if has_memb:
496
+ ll -= l2 * jnp.sum(jnp.square(gammas))
497
+ ll -= l1 * jnp.sum(jnp.abs(gammas))
498
+ return -ll
499
+
500
+ obj, grad_fn = None, None
501
+ try:
502
+ obj = self.jit(_negloglik_flat)
503
+ grad_fn = self.jit(self.value_and_grad(_negloglik_flat))
504
+ except Exception:
505
+ pass
506
+
507
+ cached = (obj, grad_fn)
508
+ setattr(self, cache_key, cached)
509
+ return cached
510
+
364
511
  def _build_jax_weighted_objective(self, X_c=None, class_idx=None):
365
512
  if X_c is None:
366
513
  cache_key = "_full"
@@ -419,7 +566,10 @@ class LatentClassMixedLogit:
419
566
 
420
567
  def objective(beta):
421
568
  value, grad = grad_fn(self.jnp.asarray(beta), weights_backend)
422
- return float(value), np.asarray(grad, dtype=float)
569
+ l2 = self.l2_penalty * float(self.jnp.sum(self.jnp.square(beta)))
570
+ l1 = self.l1_penalty * float(self.jnp.sum(self.jnp.abs(beta)))
571
+ grad_l1 = self._regularize_l1_grad(np.asarray(beta))
572
+ return float(value) + l2 + l1, np.asarray(grad, dtype=float) + 2.0 * self.l2_penalty * np.asarray(beta) + grad_l1
423
573
  else:
424
574
  def objective(beta):
425
575
  utilities = np.einsum("njk,k->nj", X_c, beta)
@@ -431,7 +581,9 @@ class LatentClassMixedLogit:
431
581
  loglik = np.sum(weights * np.log(chosen_prob))
432
582
  diff = (self.y - probs) * weights[:, None]
433
583
  grad = np.einsum("nj,njk->k", diff, X_c)
434
- return -loglik, -grad
584
+ l2 = self.l2_penalty * float(np.sum(beta * beta))
585
+ l1 = self.l1_penalty * float(np.sum(np.abs(beta)))
586
+ return -loglik + l2 + l1, -grad + 2.0 * self.l2_penalty * beta + self._regularize_l1_grad(beta)
435
587
 
436
588
  result = minimize(
437
589
  objective,
@@ -469,11 +621,18 @@ class LatentClassMixedLogit:
469
621
  y_b = self.y_backend
470
622
  av_b = self.avail_backend
471
623
  C = self.n_classes
624
+ K_util = int(self._Ks[0])
625
+ util_spec = self._class_specs[0] if self._class_specs is not None else None
626
+
627
+ if util_spec is not None and len(util_spec) < self.K:
628
+ X_util = X_b[:, :, util_spec]
629
+ else:
630
+ X_util = X_b
472
631
 
473
632
  @self.jit
474
633
  def _jax_negll(betas_flat):
475
- betas = betas_flat.reshape(C, self.K)
476
- utilities = jnp.einsum("ck,njk->ncj", betas, X_b)
634
+ betas = betas_flat.reshape(C, K_util)
635
+ utilities = jnp.einsum("ck,njk->ncj", betas, X_util)
477
636
  utilities = jnp.where(av_b[:, None, :] > 0, utilities, -1e10)
478
637
  utilities = utilities - jnp.max(utilities, axis=2, keepdims=True)
479
638
  exp_u = jnp.exp(utilities) * av_b[:, None, :]
@@ -484,7 +643,10 @@ class LatentClassMixedLogit:
484
643
  log_prior = jnp.log(jnp.full(C, 1.0 / C))
485
644
  log_joint = log_chosen + log_prior[None, :]
486
645
  log_marg = self.jax_logsumexp(log_joint, axis=1)
487
- return -jnp.sum(log_marg)
646
+ ll = jnp.sum(log_marg)
647
+ l2 = self.l2_penalty * jnp.sum(jnp.square(betas))
648
+ l1 = self.l1_penalty * jnp.sum(jnp.abs(betas))
649
+ return -(ll - l2 - l1)
488
650
 
489
651
  def _obj(betas_np):
490
652
  return float(_jax_negll(jnp.array(betas_np, dtype=jnp.float64)))
@@ -500,7 +662,10 @@ class LatentClassMixedLogit:
500
662
  log_prior = np.log(np.full(self.n_classes, 1.0 / self.n_classes))
501
663
  log_joint = log_choice + log_prior[None, :]
502
664
  log_marg = logsumexp(log_joint, axis=1)
503
- return -float(log_marg.sum())
665
+ ll = float(log_marg.sum())
666
+ ll -= self._regularize_l2_betas(betas)
667
+ ll -= self._regularize_l1_betas(betas)
668
+ return -ll
504
669
 
505
670
  K_flat = sum(self._Ks)
506
671
  print(
@@ -561,6 +726,10 @@ class LatentClassMixedLogit:
561
726
  log_denom = logsumexp(log_joint, axis=1, keepdims=True)
562
727
  posterior = np.exp(log_joint - log_denom)
563
728
  loglik = float(log_denom.sum())
729
+ loglik -= self._regularize_l2_betas(betas)
730
+ loglik -= self._regularize_l1_betas(betas)
731
+ loglik -= self._regularize_l2_gammas(gammas)
732
+ loglik -= self._regularize_l1_gammas(gammas)
564
733
 
565
734
  new_class_probs = self._normalize_class_probs(posterior.mean(axis=0))
566
735
 
@@ -585,7 +754,12 @@ class LatentClassMixedLogit:
585
754
  priors = np.broadcast_to(class_probs[None, :], (self.N, self.n_classes))
586
755
 
587
756
  log_joint = log_choice + np.log(np.clip(priors, 1e-300, None))
588
- return float(logsumexp(log_joint, axis=1).sum())
757
+ ll = float(logsumexp(log_joint, axis=1).sum())
758
+ ll -= self._regularize_l2_betas(betas)
759
+ ll -= self._regularize_l1_betas(betas)
760
+ ll -= self._regularize_l2_gammas(gammas)
761
+ ll -= self._regularize_l1_gammas(gammas)
762
+ return ll
589
763
 
590
764
  def _fit_em_once(self, rng, betas0=None, class_probs0=None, gammas0=None):
591
765
  betas = self._make_initial_betas(rng, betas0=betas0)
@@ -607,6 +781,10 @@ class LatentClassMixedLogit:
607
781
  log_denom = logsumexp(log_joint, axis=1, keepdims=True)
608
782
  posterior = np.exp(log_joint - log_denom)
609
783
  loglik = float(log_denom.sum())
784
+ loglik -= self._regularize_l2_betas(betas)
785
+ loglik -= self._regularize_l1_betas(betas)
786
+ loglik -= self._regularize_l2_gammas(gammas)
787
+ loglik -= self._regularize_l1_gammas(gammas)
610
788
 
611
789
  class_probs = self._normalize_class_probs(posterior.mean(axis=0))
612
790
  for c in range(self.n_classes):
@@ -882,15 +1060,32 @@ class LatentClassMixedLogit:
882
1060
  if maxiter is None:
883
1061
  maxiter = max(100, 100 * len(params0))
884
1062
 
885
- def _neg_ll(params):
886
- return -self._full_loglik(params)
887
-
888
- result = minimize(
889
- _neg_ll,
890
- params0,
891
- method="L-BFGS-B",
892
- options={"maxiter": maxiter, "ftol": self.tol},
893
- )
1063
+ jax_obj = self._build_jax_full_objective()
1064
+ if jax_obj is not None:
1065
+ _jax_func, _jax_grad = jax_obj
1066
+
1067
+ def _neg_ll_jax(params):
1068
+ b = self.jnp.asarray(params, dtype=self.jnp.float64)
1069
+ v, g = _jax_grad(b)
1070
+ return float(v), np.asarray(g, dtype=float)
1071
+
1072
+ result = minimize(
1073
+ _neg_ll_jax,
1074
+ params0,
1075
+ method="L-BFGS-B",
1076
+ jac=True,
1077
+ options={"maxiter": maxiter, "ftol": self.tol},
1078
+ )
1079
+ else:
1080
+ def _neg_ll(params):
1081
+ return -self._full_loglik(params)
1082
+
1083
+ result = minimize(
1084
+ _neg_ll,
1085
+ params0,
1086
+ method="L-BFGS-B",
1087
+ options={"maxiter": maxiter, "ftol": self.tol},
1088
+ )
894
1089
 
895
1090
  params = result.x
896
1091
  offset = C - 1
@@ -970,7 +1165,7 @@ class LatentClassMixedLogit:
970
1165
  else:
971
1166
  base_beta = self.class_betas[source_idx]
972
1167
  rng = np.random.default_rng(self.random_state + next_classes)
973
- jitter = rng.normal(scale=jitter_scale, size=self.K)
1168
+ jitter = rng.normal(scale=jitter_scale, size=base_beta.shape[0])
974
1169
  new_betas = np.vstack([self.class_betas, base_beta + jitter])
975
1170
 
976
1171
  new_probs = np.empty(next_classes, dtype=float)
@@ -1105,7 +1300,10 @@ class LatentClassMixedLogit:
1105
1300
  )
1106
1301
  )
1107
1302
  log_joint = log_chosen + np.log(np.clip(priors, 1e-300, None))
1108
- return float(logsumexp(log_joint, axis=1).sum())
1303
+ ll = float(logsumexp(log_joint, axis=1).sum())
1304
+ ll -= self._regularize_l2_betas(betas)
1305
+ ll -= self._regularize_l1_betas(betas)
1306
+ return ll
1109
1307
 
1110
1308
  def _numerical_hessian(self, params: np.ndarray, eps: float = 1e-4) -> np.ndarray:
1111
1309
  """Numerical Hessian of the log-likelihood via central finite differences.
@@ -1423,8 +1621,17 @@ class LatentClassMixedLogit:
1423
1621
  print(f" class_{idx}: {share:.6f}")
1424
1622
  print(" Coefficients:")
1425
1623
  for c_idx, beta in enumerate(self.class_betas, start=1):
1426
- for name, value in zip(self.varnames, beta):
1624
+ idx = self._class_specs[c_idx - 1] if self._class_specs is not None else range(len(beta))
1625
+ for k, value in zip(idx, beta):
1626
+ name = self.varnames[k]
1427
1627
  print(f" class_{c_idx}_{name}: {value:.6f}")
1628
+ if self.class_gammas is not None and self._has_membership and self.optimise_membership:
1629
+ gammas_arr = np.asarray(self.class_gammas)
1630
+ print(" Membership Parameters:")
1631
+ for c in range(self.n_classes - 1):
1632
+ for k in range(gammas_arr.shape[1]):
1633
+ label = f"gamma_{c + 1}_{self.membership_vars[k]}"
1634
+ print(f" {label}: {gammas_arr[c, k]:.6f}")
1428
1635
  return
1429
1636
 
1430
1637
  params = stats["params"]
@@ -0,0 +1 @@
1
+ 0.0.121
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SearchLibrium
3
- Version: 0.0.119
3
+ Version: 0.0.121
4
4
  Summary: A Python package for econometric models driven by search
5
5
  Author: Alexander Paz Prithvi Beeramole, Robert Burdett
6
6
  Author-email: Zeke Ahern <z.ahern@qut.edu.au>
@@ -15,6 +15,7 @@ src/SearchLibrium/boxcox_functions.py
15
15
  src/SearchLibrium/call_meta.py
16
16
  src/SearchLibrium/constraints_builder.py
17
17
  src/SearchLibrium/harmony.py
18
+ src/SearchLibrium/hspbil.py
18
19
  src/SearchLibrium/latent_class.py
19
20
  src/SearchLibrium/main.py
20
21
  src/SearchLibrium/main_debug.py
@@ -1 +0,0 @@
1
- 0.0.119