SearchLibrium 0.0.253__tar.gz → 0.0.260__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.253/src/SearchLibrium.egg-info → searchlibrium-0.0.260}/PKG-INFO +1 -1
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/pyproject.toml +2 -2
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/harmony.py +44 -24
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/hspbil.py +7 -1
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/mh_choice_estimator.py +45 -7
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/search.py +108 -1
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/sparseea_agds.py +84 -45
- searchlibrium-0.0.260/src/SearchLibrium/version.txt +1 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260/src/SearchLibrium.egg-info}/PKG-INFO +1 -1
- searchlibrium-0.0.253/src/SearchLibrium/version.txt +0 -1
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/LICENSE +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/MANIFEST.in +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/README.md +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/setup.cfg +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/BEHier.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/Halton.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/MixedLogit.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/MixedLogitGSE.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/Mode_Activity_Nested.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/RandomP.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/SEARCH_SM_MARIO.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/Two_Level_Nest.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/__init__.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/__main__.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/_choice_model.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/_device.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/abm.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/accessibility.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/banditsa.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/boxcox_functions.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/call_meta.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/constraints_builder.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/coupled_solver.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/data/Swissmetro_final.csv +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/data/TravelMode.csv +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/data/electricity.csv +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/destination_distances.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/jax_utils.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/larch_compat.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/larch_models.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/latent_class.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/latent_spec.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/logistic_regression.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/main.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/main_debug.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/mdcev.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/misc.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/mixed_logit.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/mixed_nested.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/mixedrrm.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/multinomial_logit.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/multinomial_nested.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/multinomial_probit.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/numba_engine.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/numba_mxl.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/numba_rrm.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/ordered_logit.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/ordered_logit_mixed.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/predict.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/regularization.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/rrm.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/sample_data.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/sapbil.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/selection_models.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/setup.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/siman.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/skims.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/test_lc_de.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/test_mario_searches.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/test_sapbil_vs_banditsa.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/threshold.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/zero_inflated_ordered_probit.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium.egg-info/SOURCES.txt +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium.egg-info/dependency_links.txt +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium.egg-info/entry_points.txt +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium.egg-info/requires.txt +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium.egg-info/top_level.txt +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/tests/test_destination_distances.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/tests/test_jax_mask_equivalence.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/tests/test_larch_integration.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/tests/test_lc_smart_init.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/tests/test_mh_choice_estimator.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/tests/test_mxl_random_starts.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/tests/test_per_alternative_random.py +0 -0
- {searchlibrium-0.0.253 → searchlibrium-0.0.260}/tests/test_zero_inflated_ordered_probit.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: SearchLibrium
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.260
|
|
4
4
|
Summary: Automated specification search for discrete choice models (logit, mixed logit, nested logit, RRM and more) using metaheuristic optimisation
|
|
5
5
|
Author: Mario Fernando Taco Morales, Alexander Paz, Prithvi Beeramole, Robert Burdett
|
|
6
6
|
Author-email: Zeke Ahern <z.ahern@qut.edu.au>
|
|
@@ -129,7 +129,7 @@ Documentation = "https://pypi.org/project/SearchLibrium/"
|
|
|
129
129
|
realpython = "SearchLibrium.__main__:main"
|
|
130
130
|
|
|
131
131
|
[tool.bumpver]
|
|
132
|
-
current_version = "0.0.
|
|
132
|
+
current_version = "0.0.260"
|
|
133
133
|
version_pattern = "MAJOR.MINOR.PATCH"
|
|
134
134
|
commit_message = "[skip ci] Bump version {old_version} -> {new_version}"
|
|
135
135
|
commit = true
|
|
@@ -137,4 +137,4 @@ tag = true
|
|
|
137
137
|
push = false
|
|
138
138
|
|
|
139
139
|
[tool.bumpver.file_patterns]
|
|
140
|
-
"src/SearchLibrium/version.txt" = ["{version}"]
|
|
140
|
+
"src/SearchLibrium/version.txt" = ["{version}"]
|
|
@@ -237,34 +237,49 @@ class HarmonySearch(Search):
|
|
|
237
237
|
def initialize_memory(self, nb_sols):
|
|
238
238
|
"""
|
|
239
239
|
Initialize harmony memory and opposite harmony memory with unique random solutions.
|
|
240
|
+
|
|
241
|
+
De-duplicates by model SPECIFICATION (``get_unique_spec``), not by
|
|
242
|
+
objective value (``get_unique``). Criterion 0 is usually BIC (a
|
|
243
|
+
continuous float), but many specs still share an objective value —
|
|
244
|
+
and where the objective is a coarse count (e.g. an ``nsig``-style
|
|
245
|
+
criterion) ``get_unique(mem, 0)`` collapses the whole memory to a
|
|
246
|
+
handful of entries, so the loop burns its entire attempt budget
|
|
247
|
+
without ever reaching ``nb_sols``. Only NEW opposite solutions are
|
|
248
|
+
folded in each pass (the old ``mem += opp_mem`` re-merged and
|
|
249
|
+
re-sorted the whole accumulated ``opp_mem`` every iteration — O(n^2)
|
|
250
|
+
work and needless model fits).
|
|
240
251
|
"""
|
|
241
|
-
mem
|
|
242
|
-
|
|
252
|
+
mem = []
|
|
253
|
+
seen = set() # spec keys already represented in mem
|
|
254
|
+
max_attempts = 30000 # safety ceiling on outer generation attempts
|
|
255
|
+
|
|
256
|
+
def _admit(cand):
|
|
257
|
+
"""True if ``cand`` is converged, within BOUND, and a new spec."""
|
|
258
|
+
if cand is None or not abs(cand.obj(0)) < BOUND:
|
|
259
|
+
return False
|
|
260
|
+
k = spec_key(cand)
|
|
261
|
+
if k in seen:
|
|
262
|
+
return False
|
|
263
|
+
seen.add(k)
|
|
264
|
+
mem.append(cand)
|
|
265
|
+
return True
|
|
266
|
+
|
|
267
|
+
attempts = 0
|
|
268
|
+
while len(mem) < nb_sols and attempts < max_attempts:
|
|
269
|
+
attempts += 1
|
|
243
270
|
sol = self.generate_solution() # Generate a solution
|
|
244
271
|
sol, converged = self.evaluate_solution(sol)
|
|
245
272
|
|
|
246
|
-
if converged:
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
# Generate opposite solution only if the original solution is valid
|
|
250
|
-
if sol is not None:
|
|
273
|
+
if converged and _admit(sol):
|
|
274
|
+
# Generate one opposite solution and fold it in directly
|
|
251
275
|
opp_sol = self.create_opposite_solution(sol)
|
|
252
|
-
|
|
253
276
|
if opp_sol is not None: # Validate opposite solution before evaluating
|
|
254
|
-
opp_sol,
|
|
255
|
-
if
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
mem += opp_mem # Combine memory and opposite memory
|
|
259
|
-
mem = get_unique(mem, 0) # Keep unique solutions only
|
|
260
|
-
|
|
261
|
-
# Filter memory to remove poor solutions based on the first objective
|
|
262
|
-
mem = [sol for sol in mem if abs(sol.obj(0)) < BOUND] # Call obj(0) correctly
|
|
263
|
-
|
|
264
|
-
if len(mem) >= nb_sols:
|
|
265
|
-
return mem[:nb_sols] # Return the initialized memory
|
|
277
|
+
opp_sol, o_converged = self.evaluate_solution(opp_sol)
|
|
278
|
+
if o_converged:
|
|
279
|
+
_admit(opp_sol)
|
|
266
280
|
|
|
267
|
-
|
|
281
|
+
# Keep the best nb_sols by objective (get_unique_spec sorts by obj(0))
|
|
282
|
+
return get_unique_spec(mem, 0)[:nb_sols]
|
|
268
283
|
|
|
269
284
|
''' ---------------------------------------------------------- '''
|
|
270
285
|
''' Function. Build new solution using Harmony Memory '''
|
|
@@ -1237,7 +1252,7 @@ class HarmonySearch(Search):
|
|
|
1237
1252
|
# }
|
|
1238
1253
|
|
|
1239
1254
|
''' ---------------------------------------------------------- '''
|
|
1240
|
-
''' Function. Discard non-convergent solutions
|
|
1255
|
+
''' Function. Discard non-convergent solutions '''
|
|
1241
1256
|
''' ---------------------------------------------------------- '''
|
|
1242
1257
|
def screen_solutions(self, solutions):
|
|
1243
1258
|
# {
|
|
@@ -1435,7 +1450,11 @@ class HarmonySearch(Search):
|
|
|
1435
1450
|
# OPTIONAL: CREATE max(max_mem - existing, 0) NEW SOLUTIONS?
|
|
1436
1451
|
|
|
1437
1452
|
init_memory = generated_memory + existing_memory # Aggregate solution lists
|
|
1438
|
-
|
|
1453
|
+
# De-duplicate by SPECIFICATION, not objective value: get_unique(init_memory, 0)
|
|
1454
|
+
# drops every candidate sharing an obj(0) value with one already kept, which
|
|
1455
|
+
# silently shrinks the memory below max_mem (most acute when criterion 0 is
|
|
1456
|
+
# a coarse value like an nsig-style count rather than BIC).
|
|
1457
|
+
unique_memory = get_unique_spec(init_memory, 0) # Remove duplicate specifications
|
|
1439
1458
|
for sol in unique_memory:
|
|
1440
1459
|
sol.data['is_initial_sol'] = True # Set solution status
|
|
1441
1460
|
|
|
@@ -1804,7 +1823,8 @@ class HarmonySearch(Search):
|
|
|
1804
1823
|
existing_memory = self.screen_solutions(existing_sols)
|
|
1805
1824
|
generated_memory = self.initialize_memory(self.max_mem)
|
|
1806
1825
|
init_memory = generated_memory + existing_memory
|
|
1807
|
-
|
|
1826
|
+
# De-duplicate by SPECIFICATION, not objective value (see note above).
|
|
1827
|
+
unique_memory = get_unique_spec(init_memory, 0)
|
|
1808
1828
|
for sol in unique_memory:
|
|
1809
1829
|
sol.data['is_initial_sol'] = True
|
|
1810
1830
|
|
|
@@ -21,9 +21,11 @@ import time as _time
|
|
|
21
21
|
try:
|
|
22
22
|
from .harmony import HarmonySearch
|
|
23
23
|
from .sapbil import ProbabilityMatrix
|
|
24
|
+
from .search import get_unique_spec
|
|
24
25
|
except ImportError:
|
|
25
26
|
from harmony import HarmonySearch
|
|
26
27
|
from sapbil import ProbabilityMatrix
|
|
28
|
+
from search import get_unique_spec
|
|
27
29
|
|
|
28
30
|
logger = logging.getLogger(__name__)
|
|
29
31
|
|
|
@@ -339,7 +341,11 @@ class HSPBIL(HarmonySearch):
|
|
|
339
341
|
existing_memory = self.screen_solutions(existing_sols)
|
|
340
342
|
generated_memory = self.initialize_memory(self.max_mem)
|
|
341
343
|
init_memory = generated_memory + existing_memory
|
|
342
|
-
|
|
344
|
+
# De-duplicate by SPECIFICATION, not objective value: get_unique(init_memory, 0)
|
|
345
|
+
# drops every candidate sharing an obj(0) value with one already kept, which
|
|
346
|
+
# silently shrinks the memory below max_mem (most acute when criterion 0 is
|
|
347
|
+
# a coarse value like an nsig-style count rather than BIC).
|
|
348
|
+
unique_memory = get_unique_spec(init_memory, 0)
|
|
343
349
|
for sol in unique_memory:
|
|
344
350
|
sol.data['is_initial_sol'] = True
|
|
345
351
|
|
|
@@ -481,6 +481,13 @@ class BlockRandomWalkProposal:
|
|
|
481
481
|
# ---------------------------------------------------------------------------
|
|
482
482
|
|
|
483
483
|
|
|
484
|
+
def _env_int(name: str, default: int) -> int:
|
|
485
|
+
try:
|
|
486
|
+
return int(str(os.environ.get(name, "")).strip())
|
|
487
|
+
except (TypeError, ValueError):
|
|
488
|
+
return default
|
|
489
|
+
|
|
490
|
+
|
|
484
491
|
class HHTSCompetingPrior:
|
|
485
492
|
"""Dirichlet-smoothed HHTS shares blended with current utilities."""
|
|
486
493
|
|
|
@@ -496,6 +503,8 @@ class HHTSCompetingPrior:
|
|
|
496
503
|
max_alpha: float = 0.95,
|
|
497
504
|
min_temperature: float = 0.15,
|
|
498
505
|
max_temperature: float = 8.0,
|
|
506
|
+
max_history: int | None = None,
|
|
507
|
+
record_history: bool = True,
|
|
499
508
|
) -> None:
|
|
500
509
|
raw = shares or {}
|
|
501
510
|
if raw and all(not isinstance(value, Mapping) for value in raw.values()):
|
|
@@ -512,8 +521,13 @@ class HHTSCompetingPrior:
|
|
|
512
521
|
# adapt_to_denominator_ess runs once per CHOICE CASE per Gibbs
|
|
513
522
|
# iteration, so an unbounded list here grew by n_cases x outer
|
|
514
523
|
# iterations (millions of dicts on the 13k-case datasets). Only the
|
|
515
|
-
# most recent entry is ever read, so keep a bounded window
|
|
516
|
-
|
|
524
|
+
# most recent entry is ever read, so keep a bounded window
|
|
525
|
+
# (SL_HHTS_MAX_HISTORY, default 4096; 0 disables recording).
|
|
526
|
+
if max_history is None:
|
|
527
|
+
max_history = _env_int("SL_HHTS_MAX_HISTORY", 4096)
|
|
528
|
+
self.max_history = max(0, int(max_history))
|
|
529
|
+
self.record_history = bool(record_history) and self.max_history > 0
|
|
530
|
+
self.history_maxlen: int = self.max_history or 1
|
|
517
531
|
self.history: deque = deque(maxlen=self.history_maxlen)
|
|
518
532
|
|
|
519
533
|
@classmethod
|
|
@@ -578,7 +592,8 @@ class HHTSCompetingPrior:
|
|
|
578
592
|
self.alpha = float(np.clip(self.alpha + self.adaptation_rate * error, self.min_alpha, self.max_alpha))
|
|
579
593
|
self.temperature = float(np.clip(self.temperature * np.exp(self.adaptation_rate * error),
|
|
580
594
|
self.min_temperature, self.max_temperature))
|
|
581
|
-
|
|
595
|
+
if self.record_history:
|
|
596
|
+
self.history.append({"denominator_ess": ess, "alpha": self.alpha, "temperature": self.temperature})
|
|
582
597
|
return ess
|
|
583
598
|
|
|
584
599
|
def sample_choice_sets(self, observations: pd.DataFrame, zone_ids: Sequence[object],
|
|
@@ -1265,17 +1280,29 @@ def _weighted_frame(frame: "ChoiceSetFrame",
|
|
|
1265
1280
|
frame.feature_names, tuple(float(w) for w in np.asarray(weights).ravel()))
|
|
1266
1281
|
|
|
1267
1282
|
|
|
1268
|
-
def _fit_membership_gammas(case_X, resp, l2: float = 1.0, maxiter: int = 200
|
|
1283
|
+
def _fit_membership_gammas(case_X, resp, l2: float = 1.0, maxiter: int = 200,
|
|
1284
|
+
standardize: bool = True):
|
|
1269
1285
|
"""Weighted multinomial-logit M-step for the class-membership equation.
|
|
1270
1286
|
|
|
1271
1287
|
``gammas`` has shape (C-1, K+1) with class 0 as the reference and an
|
|
1272
1288
|
intercept column. Rows are weighted by the EM responsibilities (soft
|
|
1273
1289
|
labels), so the step maximises the expected membership log-likelihood;
|
|
1274
1290
|
slopes carry an L2 penalty, intercepts do not.
|
|
1291
|
+
|
|
1292
|
+
If ``standardize=True`` (default), covariates are standardized to mean=0,
|
|
1293
|
+
std=1 before fitting, and coefficients are transformed back to the original
|
|
1294
|
+
scale for the returned ``gammas``.
|
|
1275
1295
|
"""
|
|
1276
1296
|
from scipy.optimize import minimize
|
|
1277
1297
|
N = np.asarray(case_X).shape[0]
|
|
1278
|
-
|
|
1298
|
+
X = np.asarray(case_X, dtype=float)
|
|
1299
|
+
if standardize:
|
|
1300
|
+
X_mean = X.mean(axis=0)
|
|
1301
|
+
X_std = X.std(axis=0, ddof=0)
|
|
1302
|
+
X_std = np.where(X_std > 1e-12, X_std, 1.0)
|
|
1303
|
+
Xa = np.column_stack([np.ones(N), (X - X_mean) / X_std])
|
|
1304
|
+
else:
|
|
1305
|
+
Xa = np.column_stack([np.ones(N), X])
|
|
1279
1306
|
C = np.asarray(resp).shape[1]
|
|
1280
1307
|
Ka = Xa.shape[1]
|
|
1281
1308
|
if C < 2:
|
|
@@ -1296,7 +1323,18 @@ def _fit_membership_gammas(case_X, resp, l2: float = 1.0, maxiter: int = 200):
|
|
|
1296
1323
|
|
|
1297
1324
|
res = minimize(nll, g0.ravel(), method="L-BFGS-B",
|
|
1298
1325
|
options={"maxiter": int(maxiter)})
|
|
1299
|
-
|
|
1326
|
+
G = res.x.reshape(C - 1, Ka)
|
|
1327
|
+
# Transform back to original scale if standardized
|
|
1328
|
+
if standardize:
|
|
1329
|
+
# G has shape (C-1, Ka) where Ka = n_features + 1 (intercept)
|
|
1330
|
+
# G[:, 0] are intercepts, G[:, 1:] are slopes on standardized features
|
|
1331
|
+
G_out = G.copy()
|
|
1332
|
+
G_out[:, 1:] = G[:, 1:] / X_std
|
|
1333
|
+
# Intercept adjustment: intercept = intercept - sum(mean_k * slope_k / std_k)
|
|
1334
|
+
# For each class (row), compute dot product of (mean/std) with slopes
|
|
1335
|
+
G_out[:, 0] = G[:, 0] - np.sum((X_mean / X_std) * G[:, 1:], axis=1)
|
|
1336
|
+
return G_out
|
|
1337
|
+
return G
|
|
1300
1338
|
|
|
1301
1339
|
|
|
1302
1340
|
def _fit_frame_mnl(frame: "ChoiceSetFrame", x0, *, prior_scale: float = 5.0,
|
|
@@ -1530,7 +1568,7 @@ def estimate_dest_choice_lc(
|
|
|
1530
1568
|
prior_scale=prior_scale, bounds=bounds)[0]
|
|
1531
1569
|
for c in range(n_classes)]
|
|
1532
1570
|
if case_X is not None:
|
|
1533
|
-
gammas = _fit_membership_gammas(case_X, p, l2=membership_l2)
|
|
1571
|
+
gammas = _fit_membership_gammas(case_X, p, l2=membership_l2, standardize=True)
|
|
1534
1572
|
shares = new_shares
|
|
1535
1573
|
if share_floor and float(share_floor) > 0:
|
|
1536
1574
|
_coll = int(np.argmin(shares))
|
|
@@ -60,6 +60,7 @@ from collections import UserDict
|
|
|
60
60
|
|
|
61
61
|
from enum import Enum
|
|
62
62
|
import copy
|
|
63
|
+
import itertools
|
|
63
64
|
import numpy as np
|
|
64
65
|
import pandas as pd
|
|
65
66
|
import random
|
|
@@ -215,6 +216,16 @@ def process_criterions(criterions):
|
|
|
215
216
|
# }
|
|
216
217
|
|
|
217
218
|
|
|
219
|
+
def _search_env_int(name, default):
|
|
220
|
+
''' Read an integer tuning knob from the environment, falling back to
|
|
221
|
+
`default` when it is unset or not a valid integer. '''
|
|
222
|
+
import os
|
|
223
|
+
try:
|
|
224
|
+
return int(str(os.environ.get(name, '')).strip())
|
|
225
|
+
except (TypeError, ValueError):
|
|
226
|
+
return default
|
|
227
|
+
|
|
228
|
+
|
|
218
229
|
def count_insig_groups(coeff_names, pvalues, p_val=0.05):
|
|
219
230
|
"""Count variable *groups* where *every* coefficient fails the p-value
|
|
220
231
|
threshold. A group is the set of coefficients belonging to the same
|
|
@@ -525,6 +536,64 @@ def get_unique_tuple(solutions):
|
|
|
525
536
|
return unique_solutions
|
|
526
537
|
# }
|
|
527
538
|
|
|
539
|
+
|
|
540
|
+
def spec_key(sol):
|
|
541
|
+
''' ---------------------------------------------------------- '''
|
|
542
|
+
''' Hashable key identifying a solution's model *specification*. '''
|
|
543
|
+
''' '''
|
|
544
|
+
''' Used to de-duplicate populations by spec instead of by objective '''
|
|
545
|
+
''' value. ``get_unique`` de-dupes on ``obj(key)``, which collapses '''
|
|
546
|
+
''' the population whenever that objective takes only a handful of '''
|
|
547
|
+
''' distinct values (an integer count such as ``nsig``, or tied '''
|
|
548
|
+
''' BICs) — the initialisation loops then spin their whole attempt '''
|
|
549
|
+
''' budget without ever filling the memory/population target. '''
|
|
550
|
+
''' ---------------------------------------------------------- '''
|
|
551
|
+
try:
|
|
552
|
+
_get = sol.get if hasattr(sol, 'get') else (
|
|
553
|
+
lambda k, d=None: getattr(sol, k, d))
|
|
554
|
+
|
|
555
|
+
def _norm(v):
|
|
556
|
+
try:
|
|
557
|
+
if isinstance(v, np.ndarray):
|
|
558
|
+
v = v.tolist()
|
|
559
|
+
except Exception:
|
|
560
|
+
pass
|
|
561
|
+
if isinstance(v, dict):
|
|
562
|
+
return tuple(sorted((str(k), str(x)) for k, x in v.items()))
|
|
563
|
+
if isinstance(v, (list, tuple, set, frozenset)):
|
|
564
|
+
return tuple(sorted(str(x) for x in v))
|
|
565
|
+
return str(v)
|
|
566
|
+
|
|
567
|
+
return tuple((k, _norm(_get(k))) for k in (
|
|
568
|
+
'model_n', 'asvars', 'isvars', 'randvars', 'corvars',
|
|
569
|
+
'bcvars', 'transvars', 'trans_asvars', 'asc_ind',
|
|
570
|
+
'class_params_spec', 'member_params_spec'))
|
|
571
|
+
except Exception:
|
|
572
|
+
try:
|
|
573
|
+
return ('__tuple__', tuple(sol.values()))
|
|
574
|
+
except Exception:
|
|
575
|
+
return ('__repr__', repr(sol))
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def get_unique_spec(solutions, key=0):
|
|
579
|
+
''' ---------------------------------------------------------- '''
|
|
580
|
+
''' Like ``get_unique`` but de-duplicates by specification, not '''
|
|
581
|
+
''' objective. Keeps ``get_unique``'s ordering (sorted by objective '''
|
|
582
|
+
''' ``key``) so callers see the same ranking with by-spec '''
|
|
583
|
+
''' duplicates removed. '''
|
|
584
|
+
''' ---------------------------------------------------------- '''
|
|
585
|
+
if not solutions:
|
|
586
|
+
return []
|
|
587
|
+
ordered = sorted(solutions, key=lambda sol: sol.obj(key))
|
|
588
|
+
seen, out = set(), []
|
|
589
|
+
for sol in ordered:
|
|
590
|
+
k = spec_key(sol)
|
|
591
|
+
if k not in seen:
|
|
592
|
+
seen.add(k)
|
|
593
|
+
out.append(sol)
|
|
594
|
+
return out
|
|
595
|
+
|
|
596
|
+
|
|
528
597
|
''' ---------------------------------------------------------- '''
|
|
529
598
|
''' FUNCTION. CREATE TEST DATASET '''
|
|
530
599
|
''' ---------------------------------------------------------- '''
|
|
@@ -1497,6 +1566,18 @@ class Search():
|
|
|
1497
1566
|
self.explored_specs = set() # Set of hashes for all explored specifications (evaluated or not)
|
|
1498
1567
|
self.cache_hits = 0 # Count of how many times a solution was found in the cache
|
|
1499
1568
|
|
|
1569
|
+
# Cache growth bound (SEARCHLIBRIUM_CACHE_LIMIT, default 250000).
|
|
1570
|
+
# evaluated_solutions / explored_specs / _banlist are keyed by a
|
|
1571
|
+
# 64-char signature and grow by one entry per evaluated individual.
|
|
1572
|
+
# Over a long evolutionary search (tens of thousands of fits) that is
|
|
1573
|
+
# tens of MB of dead weight, and a specification-search job can then
|
|
1574
|
+
# trip a cgroup memory limit that the live population never came
|
|
1575
|
+
# close to. Cap them and drop the oldest entries under pressure.
|
|
1576
|
+
# Both caches are pure speed/heuristic aids: evicting an entry costs a
|
|
1577
|
+
# refit (or one retry of an already-failed spec), never correctness.
|
|
1578
|
+
self._cache_limit = _search_env_int('SEARCHLIBRIUM_CACHE_LIMIT', 250000)
|
|
1579
|
+
self._n_explored = 0 # monotonic count; survives cache trimming
|
|
1580
|
+
|
|
1500
1581
|
self.last_printed_solution = None # Track the last printed solution
|
|
1501
1582
|
self.best_solution = None # T
|
|
1502
1583
|
|
|
@@ -4801,12 +4882,37 @@ class Search():
|
|
|
4801
4882
|
''' and cache the result if it converged. '''
|
|
4802
4883
|
''' ------------------------------------------------------------------ '''
|
|
4803
4884
|
|
|
4885
|
+
def _trim_search_caches(self):
|
|
4886
|
+
''' Drop the oldest entries from the specification caches once they
|
|
4887
|
+
exceed `self._cache_limit`.
|
|
4888
|
+
|
|
4889
|
+
`evaluated_solutions` is insertion-ordered, so the front is the
|
|
4890
|
+
oldest. `explored_specs` and `_banlist` are unordered sets whose
|
|
4891
|
+
only role is bookkeeping / avoiding a retry of a known-bad spec, so
|
|
4892
|
+
they are cleared wholesale when oversized. `self._n_explored` keeps
|
|
4893
|
+
the true running total for the exploration summary. '''
|
|
4894
|
+
limit = getattr(self, '_cache_limit', 0)
|
|
4895
|
+
if not limit:
|
|
4896
|
+
return
|
|
4897
|
+
if len(self.evaluated_solutions) > limit:
|
|
4898
|
+
# free ~25% in one pass rather than trimming on every single insert
|
|
4899
|
+
for _sig in list(itertools.islice(self.evaluated_solutions,
|
|
4900
|
+
len(self.evaluated_solutions) - limit // 4)):
|
|
4901
|
+
self.evaluated_solutions.pop(_sig, None)
|
|
4902
|
+
logging.debug('trimmed evaluated_solutions to %d (limit %d)',
|
|
4903
|
+
len(self.evaluated_solutions), limit)
|
|
4904
|
+
if len(self.explored_specs) > limit:
|
|
4905
|
+
self.explored_specs.clear()
|
|
4906
|
+
if len(self._banlist) > limit:
|
|
4907
|
+
self._banlist.clear()
|
|
4908
|
+
|
|
4804
4909
|
def evaluate_solution(self, sol, track_best=True):
|
|
4805
4910
|
|
|
4806
4911
|
sig = self.setup_signature(sol)
|
|
4807
4912
|
|
|
4808
4913
|
if sig not in self.explored_specs:
|
|
4809
4914
|
self.explored_specs.add(sig)
|
|
4915
|
+
self._n_explored += 1
|
|
4810
4916
|
|
|
4811
4917
|
if sig in self.evaluated_solutions:
|
|
4812
4918
|
self.cache_hits += 1
|
|
@@ -4819,6 +4925,7 @@ class Search():
|
|
|
4819
4925
|
|
|
4820
4926
|
if converged:
|
|
4821
4927
|
self.evaluated_solutions[sig] = [sol.obj(i) for i in range(self.nb_crit)]
|
|
4928
|
+
self._trim_search_caches()
|
|
4822
4929
|
|
|
4823
4930
|
return sol, converged
|
|
4824
4931
|
|
|
@@ -5066,7 +5173,7 @@ class Search():
|
|
|
5066
5173
|
served from cache instead of re-fitting the model. """
|
|
5067
5174
|
|
|
5068
5175
|
sep = '═' * 60
|
|
5069
|
-
total = len(self.explored_specs)
|
|
5176
|
+
total = getattr(self, '_n_explored', len(self.explored_specs))
|
|
5070
5177
|
hits = self.cache_hits
|
|
5071
5178
|
hit_pct = (hits / total * 100) if total else 0.0
|
|
5072
5179
|
|
|
@@ -37,7 +37,9 @@ object and call ``run()`` (which dispatches to ``run_search``).
|
|
|
37
37
|
|
|
38
38
|
import copy
|
|
39
39
|
import datetime
|
|
40
|
+
import gc as _gc
|
|
40
41
|
import logging
|
|
42
|
+
import os
|
|
41
43
|
|
|
42
44
|
import numpy as np
|
|
43
45
|
|
|
@@ -48,61 +50,89 @@ except ImportError: # running as a top-level module
|
|
|
48
50
|
|
|
49
51
|
logger = logging.getLogger(__name__)
|
|
50
52
|
|
|
53
|
+
# ``spec_key`` / ``get_unique_spec`` (de-duplicate a population by model
|
|
54
|
+
# SPECIFICATION rather than by objective value) now live in search.py so every
|
|
55
|
+
# algorithm shares them. The originals were defined here; _agds_spec_key is kept
|
|
56
|
+
# as an alias for backwards compatibility.
|
|
57
|
+
_agds_spec_key = spec_key
|
|
58
|
+
|
|
51
59
|
|
|
52
60
|
# ---------------------------------------------------------------------- #
|
|
53
|
-
#
|
|
61
|
+
# Memory reclamation #
|
|
54
62
|
# ---------------------------------------------------------------------- #
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
# A long AGDS run re-fits one model per evaluated individual - tens of
|
|
64
|
+
# thousands over a 500-generation search. Each fit materialises a full
|
|
65
|
+
# case x alternative design frame (and, under a JAX backend, a freshly
|
|
66
|
+
# compiled executable), so the transient peak per fit is orders of magnitude
|
|
67
|
+
# larger than the live population. Those objects *are* released, but glibc
|
|
68
|
+
# retains the freed spans in its arenas, so RSS ratchets steadily upward and a
|
|
69
|
+
# process holding only a few hundred MB of live data still walks into a
|
|
70
|
+
# cgroup memory limit and gets OOM-killed.
|
|
71
|
+
#
|
|
72
|
+
# Calling collect() + malloc_trim() once per generation returns those spans to
|
|
73
|
+
# the OS so RSS tracks the live set instead of the high-water mark. Every
|
|
74
|
+
# CLEAR_JAX_EVERY generations (and every force=True release) the JAX
|
|
75
|
+
# compilation cache is dropped as well: one executable per distinct spec is
|
|
76
|
+
# retained there beyond the reach of gc/malloc_trim (+24.3 MB/fit measured
|
|
77
|
+
# Oct-2026 on a 1016-trip x 233-destination larch workload, identical across
|
|
78
|
+
# three runs, OOM-killing two 128 GB jobs in the refit that follows the
|
|
79
|
+
# search). Recompile cost is bounded: only re-visited specs recompile, and
|
|
80
|
+
# an evolutionary search rarely revisits.
|
|
81
|
+
#
|
|
82
|
+
# SEARCHLIBRIUM_RECLAIM_EVERY=<n> reclaim every <n> generations (default 1)
|
|
83
|
+
# SEARCHLIBRIUM_RECLAIM=0 disable entirely
|
|
84
|
+
# SEARCHLIBRIUM_CLEAR_JAX=1 also drop JAX's cache on EVERY call
|
|
85
|
+
# SEARCHLIBRIUM_CLEAR_JAX_EVERY=<n>
|
|
86
|
+
# ... otherwise drop it every <n>
|
|
87
|
+
# generations (default 5) and on every
|
|
88
|
+
# force=True release
|
|
89
|
+
|
|
90
|
+
def _env_int(name, default):
|
|
63
91
|
try:
|
|
64
|
-
|
|
65
|
-
|
|
92
|
+
return int(str(os.environ.get(name, "")).strip())
|
|
93
|
+
except (TypeError, ValueError):
|
|
94
|
+
return default
|
|
66
95
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
96
|
+
|
|
97
|
+
RECLAIM_EVERY = max(1, _env_int("SEARCHLIBRIUM_RECLAIM_EVERY", 1))
|
|
98
|
+
RECLAIM_ENABLED = str(os.environ.get("SEARCHLIBRIUM_RECLAIM", "1")).strip().lower() \
|
|
99
|
+
not in {"0", "false", "no", "off"}
|
|
100
|
+
CLEAR_JAX = str(os.environ.get("SEARCHLIBRIUM_CLEAR_JAX", "0")).strip().lower() \
|
|
101
|
+
in {"1", "true", "yes", "on"}
|
|
102
|
+
CLEAR_JAX_EVERY = max(1, _env_int("SEARCHLIBRIUM_CLEAR_JAX_EVERY", 5))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _malloc_trim():
|
|
106
|
+
"""Return free glibc heap arenas to the OS. No-op on non-glibc platforms."""
|
|
107
|
+
try:
|
|
108
|
+
import ctypes
|
|
109
|
+
libc = ctypes.CDLL("libc.so.6")
|
|
110
|
+
malloc_trim = libc.malloc_trim
|
|
111
|
+
malloc_trim.argtypes = [ctypes.c_size_t]
|
|
112
|
+
malloc_trim.restype = ctypes.c_int
|
|
113
|
+
malloc_trim(0)
|
|
83
114
|
except Exception:
|
|
84
|
-
|
|
85
|
-
return ('__tuple__', tuple(sol.values()))
|
|
86
|
-
except Exception:
|
|
87
|
-
return ('__repr__', repr(sol))
|
|
115
|
+
pass
|
|
88
116
|
|
|
89
117
|
|
|
90
|
-
def
|
|
91
|
-
"""
|
|
118
|
+
def reclaim_memory(force=False, clear_jax=False):
|
|
119
|
+
"""Collect search-local garbage and hand freed heap back to the OS.
|
|
92
120
|
|
|
93
|
-
|
|
94
|
-
|
|
121
|
+
``force=True`` releases (one-off phase transitions) always also drop
|
|
122
|
+
JAX's compiled-executable cache; in-loop callers pass ``clear_jax=True``
|
|
123
|
+
every ``CLEAR_JAX_EVERY`` generations; ``SEARCHLIBRIUM_CLEAR_JAX=1``
|
|
124
|
+
clears on every call.
|
|
95
125
|
"""
|
|
96
|
-
if not
|
|
97
|
-
return
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
126
|
+
if not (force or RECLAIM_ENABLED):
|
|
127
|
+
return
|
|
128
|
+
_gc.collect()
|
|
129
|
+
if clear_jax or force or CLEAR_JAX:
|
|
130
|
+
try:
|
|
131
|
+
import jax
|
|
132
|
+
jax.clear_caches()
|
|
133
|
+
except Exception:
|
|
134
|
+
pass
|
|
135
|
+
_malloc_trim()
|
|
106
136
|
|
|
107
137
|
|
|
108
138
|
# ---------------------------------------------------------------------- #
|
|
@@ -485,6 +515,15 @@ class SparseEAAGDS(Search):
|
|
|
485
515
|
pf.flush()
|
|
486
516
|
except Exception:
|
|
487
517
|
pass
|
|
518
|
+
|
|
519
|
+
# Return the per-generation fit garbage to the OS. Without
|
|
520
|
+
# this RSS follows the high-water mark of the transient fit
|
|
521
|
+
# buffers and a long search is OOM-killed despite a small live
|
|
522
|
+
# set. Every CLEAR_JAX_EVERY generations also drop JAX's
|
|
523
|
+
# global compilation cache (see reclaim_memory).
|
|
524
|
+
if (gen + 1) % RECLAIM_EVERY == 0:
|
|
525
|
+
reclaim_memory(
|
|
526
|
+
clear_jax=((gen + 1) % (RECLAIM_EVERY * CLEAR_JAX_EVERY) == 0))
|
|
488
527
|
finally:
|
|
489
528
|
pf.close()
|
|
490
529
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.0.260
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: SearchLibrium
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.260
|
|
4
4
|
Summary: Automated specification search for discrete choice models (logit, mixed logit, nested logit, RRM and more) using metaheuristic optimisation
|
|
5
5
|
Author: Mario Fernando Taco Morales, Alexander Paz, Prithvi Beeramole, Robert Burdett
|
|
6
6
|
Author-email: Zeke Ahern <z.ahern@qut.edu.au>
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.0.253
|
|
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
|
|
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.253 → searchlibrium-0.0.260}/src/SearchLibrium/test_sapbil_vs_banditsa.py
RENAMED
|
File without changes
|
|
File without changes
|
{searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium/zero_inflated_ordered_probit.py
RENAMED
|
File without changes
|
|
File without changes
|
{searchlibrium-0.0.253 → searchlibrium-0.0.260}/src/SearchLibrium.egg-info/dependency_links.txt
RENAMED
|
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
|