SearchLibrium 0.0.254__tar.gz → 0.0.261__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.254/src/SearchLibrium.egg-info → searchlibrium-0.0.261}/PKG-INFO +3 -3
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/pyproject.toml +6 -3
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/harmony.py +1 -1
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/mh_choice_estimator.py +45 -7
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/search.py +50 -1
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/sparseea_agds.py +89 -0
- searchlibrium-0.0.261/src/SearchLibrium/version.txt +1 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261/src/SearchLibrium.egg-info}/PKG-INFO +3 -3
- searchlibrium-0.0.254/src/SearchLibrium/version.txt +0 -1
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/LICENSE +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/MANIFEST.in +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/README.md +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/setup.cfg +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/BEHier.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/Halton.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/MixedLogit.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/MixedLogitGSE.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/Mode_Activity_Nested.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/RandomP.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/SEARCH_SM_MARIO.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/Two_Level_Nest.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/__init__.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/__main__.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/_choice_model.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/_device.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/abm.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/accessibility.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/banditsa.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/boxcox_functions.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/call_meta.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/constraints_builder.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/coupled_solver.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/data/Swissmetro_final.csv +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/data/TravelMode.csv +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/data/electricity.csv +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/destination_distances.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/hspbil.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/jax_utils.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/larch_compat.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/larch_models.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/latent_class.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/latent_spec.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/logistic_regression.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/main.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/main_debug.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/mdcev.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/misc.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/mixed_logit.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/mixed_nested.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/mixedrrm.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/multinomial_logit.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/multinomial_nested.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/multinomial_probit.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/numba_engine.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/numba_mxl.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/numba_rrm.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/ordered_logit.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/ordered_logit_mixed.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/predict.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/regularization.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/rrm.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/sample_data.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/sapbil.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/selection_models.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/setup.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/siman.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/skims.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/test_lc_de.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/test_mario_searches.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/test_sapbil_vs_banditsa.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/threshold.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/zero_inflated_ordered_probit.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium.egg-info/SOURCES.txt +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium.egg-info/dependency_links.txt +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium.egg-info/entry_points.txt +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium.egg-info/requires.txt +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium.egg-info/top_level.txt +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/tests/test_destination_distances.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/tests/test_jax_mask_equivalence.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/tests/test_larch_integration.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/tests/test_lc_smart_init.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/tests/test_mh_choice_estimator.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/tests/test_mxl_random_starts.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/tests/test_per_alternative_random.py +0 -0
- {searchlibrium-0.0.254 → searchlibrium-0.0.261}/tests/test_zero_inflated_ordered_probit.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: SearchLibrium
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.261
|
|
4
4
|
Summary: Automated specification search for discrete choice models (logit, mixed logit, nested logit, RRM and more) using metaheuristic optimisation
|
|
5
|
-
Author: Mario Fernando Taco Morales, Alexander Paz, Prithvi Beeramole, Robert Burdett
|
|
6
|
-
|
|
5
|
+
Author: Zeke Ahern, Mario Fernando Taco Morales, Alexander Paz, Prithvi Beeramole, Robert Burdett
|
|
6
|
+
Maintainer-email: Zeke Ahern <z.ahern@qut.edu.au>
|
|
7
7
|
License-Expression: MIT
|
|
8
8
|
Project-URL: Homepage, https://pypi.org/project/SearchLibrium/
|
|
9
9
|
Project-URL: Repository, https://github.com/zahern/HypothesisX
|
|
@@ -25,12 +25,15 @@ requires-python = ">=3.9"
|
|
|
25
25
|
license = "MIT"
|
|
26
26
|
license-files = ["LICENSE"]
|
|
27
27
|
authors = [
|
|
28
|
-
{ name = "Zeke Ahern"
|
|
28
|
+
{ name = "Zeke Ahern" },
|
|
29
29
|
{ name = "Mario Fernando Taco Morales" },
|
|
30
30
|
{ name = "Alexander Paz" },
|
|
31
31
|
{ name = "Prithvi Beeramole" },
|
|
32
32
|
{ name = "Robert Burdett" },
|
|
33
33
|
]
|
|
34
|
+
maintainers = [
|
|
35
|
+
{ name = "Zeke Ahern", email = "z.ahern@qut.edu.au" },
|
|
36
|
+
]
|
|
34
37
|
keywords = [
|
|
35
38
|
"econometrics",
|
|
36
39
|
"discrete choice",
|
|
@@ -129,7 +132,7 @@ Documentation = "https://pypi.org/project/SearchLibrium/"
|
|
|
129
132
|
realpython = "SearchLibrium.__main__:main"
|
|
130
133
|
|
|
131
134
|
[tool.bumpver]
|
|
132
|
-
current_version = "0.0.
|
|
135
|
+
current_version = "0.0.261"
|
|
133
136
|
version_pattern = "MAJOR.MINOR.PATCH"
|
|
134
137
|
commit_message = "[skip ci] Bump version {old_version} -> {new_version}"
|
|
135
138
|
commit = true
|
|
@@ -137,4 +140,4 @@ tag = true
|
|
|
137
140
|
push = false
|
|
138
141
|
|
|
139
142
|
[tool.bumpver.file_patterns]
|
|
140
|
-
"src/SearchLibrium/version.txt" = ["{version}"]
|
|
143
|
+
"src/SearchLibrium/version.txt" = ["{version}"]
|
|
@@ -1252,7 +1252,7 @@ class HarmonySearch(Search):
|
|
|
1252
1252
|
# }
|
|
1253
1253
|
|
|
1254
1254
|
''' ---------------------------------------------------------- '''
|
|
1255
|
-
''' Function. Discard non-convergent solutions
|
|
1255
|
+
''' Function. Discard non-convergent solutions '''
|
|
1256
1256
|
''' ---------------------------------------------------------- '''
|
|
1257
1257
|
def screen_solutions(self, solutions):
|
|
1258
1258
|
# {
|
|
@@ -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
|
|
@@ -1555,6 +1566,18 @@ class Search():
|
|
|
1555
1566
|
self.explored_specs = set() # Set of hashes for all explored specifications (evaluated or not)
|
|
1556
1567
|
self.cache_hits = 0 # Count of how many times a solution was found in the cache
|
|
1557
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
|
+
|
|
1558
1581
|
self.last_printed_solution = None # Track the last printed solution
|
|
1559
1582
|
self.best_solution = None # T
|
|
1560
1583
|
|
|
@@ -4859,12 +4882,37 @@ class Search():
|
|
|
4859
4882
|
''' and cache the result if it converged. '''
|
|
4860
4883
|
''' ------------------------------------------------------------------ '''
|
|
4861
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
|
+
|
|
4862
4909
|
def evaluate_solution(self, sol, track_best=True):
|
|
4863
4910
|
|
|
4864
4911
|
sig = self.setup_signature(sol)
|
|
4865
4912
|
|
|
4866
4913
|
if sig not in self.explored_specs:
|
|
4867
4914
|
self.explored_specs.add(sig)
|
|
4915
|
+
self._n_explored += 1
|
|
4868
4916
|
|
|
4869
4917
|
if sig in self.evaluated_solutions:
|
|
4870
4918
|
self.cache_hits += 1
|
|
@@ -4877,6 +4925,7 @@ class Search():
|
|
|
4877
4925
|
|
|
4878
4926
|
if converged:
|
|
4879
4927
|
self.evaluated_solutions[sig] = [sol.obj(i) for i in range(self.nb_crit)]
|
|
4928
|
+
self._trim_search_caches()
|
|
4880
4929
|
|
|
4881
4930
|
return sol, converged
|
|
4882
4931
|
|
|
@@ -5124,7 +5173,7 @@ class Search():
|
|
|
5124
5173
|
served from cache instead of re-fitting the model. """
|
|
5125
5174
|
|
|
5126
5175
|
sep = '═' * 60
|
|
5127
|
-
total = len(self.explored_specs)
|
|
5176
|
+
total = getattr(self, '_n_explored', len(self.explored_specs))
|
|
5128
5177
|
hits = self.cache_hits
|
|
5129
5178
|
hit_pct = (hits / total * 100) if total else 0.0
|
|
5130
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
|
|
|
@@ -55,6 +57,84 @@ logger = logging.getLogger(__name__)
|
|
|
55
57
|
_agds_spec_key = spec_key
|
|
56
58
|
|
|
57
59
|
|
|
60
|
+
# ---------------------------------------------------------------------- #
|
|
61
|
+
# Memory reclamation #
|
|
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):
|
|
91
|
+
try:
|
|
92
|
+
return int(str(os.environ.get(name, "")).strip())
|
|
93
|
+
except (TypeError, ValueError):
|
|
94
|
+
return default
|
|
95
|
+
|
|
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)
|
|
114
|
+
except Exception:
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def reclaim_memory(force=False, clear_jax=False):
|
|
119
|
+
"""Collect search-local garbage and hand freed heap back to the OS.
|
|
120
|
+
|
|
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.
|
|
125
|
+
"""
|
|
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()
|
|
136
|
+
|
|
137
|
+
|
|
58
138
|
# ---------------------------------------------------------------------- #
|
|
59
139
|
# NSGA-III reference-point helpers (numpy, self-contained) #
|
|
60
140
|
# ---------------------------------------------------------------------- #
|
|
@@ -435,6 +515,15 @@ class SparseEAAGDS(Search):
|
|
|
435
515
|
pf.flush()
|
|
436
516
|
except Exception:
|
|
437
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))
|
|
438
527
|
finally:
|
|
439
528
|
pf.close()
|
|
440
529
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.0.261
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: SearchLibrium
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.261
|
|
4
4
|
Summary: Automated specification search for discrete choice models (logit, mixed logit, nested logit, RRM and more) using metaheuristic optimisation
|
|
5
|
-
Author: Mario Fernando Taco Morales, Alexander Paz, Prithvi Beeramole, Robert Burdett
|
|
6
|
-
|
|
5
|
+
Author: Zeke Ahern, Mario Fernando Taco Morales, Alexander Paz, Prithvi Beeramole, Robert Burdett
|
|
6
|
+
Maintainer-email: Zeke Ahern <z.ahern@qut.edu.au>
|
|
7
7
|
License-Expression: MIT
|
|
8
8
|
Project-URL: Homepage, https://pypi.org/project/SearchLibrium/
|
|
9
9
|
Project-URL: Repository, https://github.com/zahern/HypothesisX
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.0.254
|
|
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
|
|
File without changes
|
{searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/test_sapbil_vs_banditsa.py
RENAMED
|
File without changes
|
|
File without changes
|
{searchlibrium-0.0.254 → searchlibrium-0.0.261}/src/SearchLibrium/zero_inflated_ordered_probit.py
RENAMED
|
File without changes
|
|
File without changes
|
{searchlibrium-0.0.254 → searchlibrium-0.0.261}/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
|