SearchLibrium 0.0.84__tar.gz → 0.0.85__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 (46) hide show
  1. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/PKG-INFO +1 -1
  2. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/pyproject.toml +1 -1
  3. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium/__init__.py +11 -2
  4. searchlibrium-0.0.85/src/SearchLibrium/mdcev.py +344 -0
  5. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium/siman.py +1 -1
  6. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium/threshold.py +1 -1
  7. searchlibrium-0.0.85/src/SearchLibrium/version.txt +1 -0
  8. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium.egg-info/PKG-INFO +1 -1
  9. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium.egg-info/SOURCES.txt +1 -0
  10. searchlibrium-0.0.84/src/SearchLibrium/version.txt +0 -1
  11. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/README.md +0 -0
  12. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/setup.cfg +0 -0
  13. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium/Halton.py +0 -0
  14. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium/MixedLogit.py +0 -0
  15. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium/Mode_Activity_Nested.py +0 -0
  16. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium/RandomP.py +0 -0
  17. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium/SEARCH_SM_MARIO.py +0 -0
  18. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium/Two_Level_Nest.py +0 -0
  19. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium/__main__.py +0 -0
  20. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium/_choice_model.py +0 -0
  21. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium/_device.py +0 -0
  22. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium/bhhh/minimize.py +0 -0
  23. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium/boxcox_functions.py +0 -0
  24. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium/call_meta.py +0 -0
  25. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium/constraints_builder.py +0 -0
  26. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium/harmony.py +0 -0
  27. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium/latent_class.py +0 -0
  28. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium/main.py +0 -0
  29. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium/main_debug.py +0 -0
  30. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium/misc.py +0 -0
  31. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium/mixed_logit.py +0 -0
  32. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium/mixed_nested.py +0 -0
  33. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium/mixedrrm.py +0 -0
  34. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium/multinomial_logit.py +0 -0
  35. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium/multinomial_nested.py +0 -0
  36. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium/multinomial_probit.py +0 -0
  37. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium/ordered_logit.py +0 -0
  38. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium/ordered_logit_mixed.py +0 -0
  39. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium/rrm.py +0 -0
  40. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium/search.py +0 -0
  41. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium/selection_models.py +0 -0
  42. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium/setup.py +0 -0
  43. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium.egg-info/dependency_links.txt +0 -0
  44. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium.egg-info/entry_points.txt +0 -0
  45. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/src/SearchLibrium.egg-info/requires.txt +0 -0
  46. {searchlibrium-0.0.84 → searchlibrium-0.0.85}/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.84
3
+ Version: 0.0.85
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>
@@ -59,7 +59,7 @@ Homepage = "https://github.com/zahern/HypothesisX"
59
59
  realpython = "SearchLibrium.__main__:main"
60
60
 
61
61
  [tool.bumpver]
62
- current_version = "0.0.84"
62
+ current_version = "0.0.85"
63
63
  version_pattern = "MAJOR.MINOR.PATCH"
64
64
  commit_message = "[skip ci] Bump version {old_version} -> {new_version}"
65
65
  commit = true
@@ -55,7 +55,13 @@ def new_features():
55
55
  """)
56
56
 
57
57
  def get_version_from_pkg_info():
58
- """Reads the version from the PKG-INFO file."""
58
+ """Reads the installed package version via importlib.metadata."""
59
+ try:
60
+ from importlib.metadata import version as _pkg_version
61
+ return _pkg_version("SearchLibrium")
62
+ except Exception:
63
+ pass
64
+ # Fallback: read from egg-info PKG-INFO (editable installs)
59
65
  pkg_info_path = os.path.join(os.path.dirname(__file__), "../SearchLibrium.egg-info/PKG-INFO")
60
66
  try:
61
67
  with open(pkg_info_path, "r") as f:
@@ -63,7 +69,8 @@ def get_version_from_pkg_info():
63
69
  if line.startswith("Version:"):
64
70
  return line.split(":")[1].strip()
65
71
  except FileNotFoundError:
66
- return "0.0.32"
72
+ pass
73
+ return "unknown"
67
74
 
68
75
  __version__ = get_version_from_pkg_info()
69
76
 
@@ -88,6 +95,7 @@ try:
88
95
  from .ordered_logit import OrderedLogit, OrderedLogitLong
89
96
  from .selection_models import BinaryProbit, HeckmanTwoStep
90
97
  from .latent_class import LatentClassMixedLogit
98
+ from .mdcev import MDCEVFitResult, MDCEVModel
91
99
  from .multinomial_probit import MultinomialProbit
92
100
  from .RandomP import RandomParameters
93
101
  from .constraints_builder import ConstraintBuilder, create_constraints
@@ -106,6 +114,7 @@ except ImportError as e:
106
114
  from ordered_logit import OrderedLogit, OrderedLogitLong
107
115
  from selection_models import BinaryProbit, HeckmanTwoStep
108
116
  from latent_class import LatentClassMixedLogit
117
+ from mdcev import MDCEVFitResult, MDCEVModel
109
118
  from multinomial_probit import MultinomialProbit
110
119
  from RandomP import RandomParameters
111
120
  from constraints_builder import ConstraintBuilder, create_constraints
@@ -0,0 +1,344 @@
1
+ """MDCEV budget-allocation prototype for SearchLibrium.
2
+
3
+ This module implements a compact translated-utility MDCEV-style allocator for
4
+ continuous budget splits such as daily time-use or discretionary activity
5
+ budgets. The implementation is forecasting-oriented: it provides a stable
6
+ fitting heuristic from observed allocations together with an analytical
7
+ budget-allocation solver based on the translated utility first-order
8
+ conditions.
9
+
10
+ The class is intended as a practical bridge between the current scalar budget
11
+ models and a fuller MDCEV pipeline. It includes both a stable heuristic fit
12
+ and a likelihood-based quasi-MLE refinement.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from dataclasses import dataclass
18
+ from typing import Iterable, Optional
19
+
20
+ import numpy as np
21
+ import pandas as pd
22
+ from scipy.optimize import minimize
23
+
24
+
25
+ def _as_2d_float(array_like) -> np.ndarray:
26
+ arr = np.asarray(array_like, dtype=float)
27
+ if arr.ndim == 1:
28
+ arr = arr.reshape(1, -1)
29
+ if arr.ndim != 2:
30
+ raise ValueError("Expected a 2D array of allocations")
31
+ return np.nan_to_num(arr, nan=0.0, posinf=0.0, neginf=0.0)
32
+
33
+
34
+ @dataclass
35
+ class MDCEVFitResult:
36
+ labels: list[str]
37
+ baseline_utility: np.ndarray
38
+ alpha: np.ndarray
39
+ gamma: np.ndarray
40
+ participation_rate: np.ndarray
41
+ mean_allocation: np.ndarray
42
+ mean_budget: float
43
+
44
+
45
+ class MDCEVModel:
46
+ """Translated-utility MDCEV-style allocator.
47
+
48
+ Parameters are learned from observed budget shares using stable moment-based
49
+ heuristics, then predictions are produced by solving the translated-utility
50
+ KKT system with a bisection search on the shadow price.
51
+ """
52
+
53
+ def __init__(
54
+ self,
55
+ outside_good: Optional[int] = 0,
56
+ alpha_floor: float = 0.05,
57
+ alpha_cap: float = 0.95,
58
+ gamma_floor: float = 1e-3,
59
+ tol: float = 1e-9,
60
+ ):
61
+ self.outside_good = outside_good
62
+ self.alpha_floor = alpha_floor
63
+ self.alpha_cap = alpha_cap
64
+ self.gamma_floor = gamma_floor
65
+ self.tol = tol
66
+
67
+ self.labels_: list[str] | None = None
68
+ self.baseline_utility_: np.ndarray | None = None
69
+ self.alpha_: np.ndarray | None = None
70
+ self.gamma_: np.ndarray | None = None
71
+ self.fit_result_: MDCEVFitResult | None = None
72
+
73
+ def fit(self, allocations, labels: Optional[Iterable[str]] = None):
74
+ """Estimate baseline utility and satiation terms from observed allocations.
75
+
76
+ Parameters
77
+ ----------
78
+ allocations:
79
+ Matrix of observed budgets split across alternatives. Rows are
80
+ observations and columns are alternatives.
81
+ labels:
82
+ Optional alternative labels.
83
+ """
84
+ y = _as_2d_float(allocations)
85
+ n_obs, n_alt = y.shape
86
+ budgets = y.sum(axis=1)
87
+ if np.any(budgets < self.tol):
88
+ raise ValueError("Each observation must have a positive total budget")
89
+
90
+ labels_list = list(labels) if labels is not None else [f"alt_{i}" for i in range(n_alt)]
91
+ if len(labels_list) != n_alt:
92
+ raise ValueError("labels length must match number of alternatives")
93
+
94
+ positive = y > self.tol
95
+ participation = positive.mean(axis=0)
96
+ mean_allocation = y.mean(axis=0)
97
+ share = y.sum(axis=0) / np.clip(y.sum(), self.tol, None)
98
+
99
+ if self.outside_good is not None and 0 <= self.outside_good < n_alt:
100
+ ref_share = max(float(share[self.outside_good]), self.tol)
101
+ baseline = np.log(np.clip(share, self.tol, None)) - np.log(ref_share)
102
+ baseline[self.outside_good] = 0.0
103
+ else:
104
+ baseline = np.log(np.clip(share, self.tol, None))
105
+ baseline = baseline - baseline.mean()
106
+
107
+ gamma = np.full(n_alt, self.gamma_floor, dtype=float)
108
+ alpha = np.full(n_alt, 0.5, dtype=float)
109
+
110
+ for idx in range(n_alt):
111
+ pos_vals = y[positive[:, idx], idx]
112
+ if pos_vals.size == 0:
113
+ gamma[idx] = max(np.median(budgets) * 0.05, self.gamma_floor)
114
+ alpha[idx] = self.alpha_floor
115
+ baseline[idx] = min(baseline[idx], -8.0)
116
+ continue
117
+
118
+ median_pos = float(np.median(pos_vals))
119
+ mean_pos = float(np.mean(pos_vals))
120
+ std_pos = float(np.std(pos_vals))
121
+ cv_pos = std_pos / max(mean_pos, self.tol)
122
+
123
+ gamma[idx] = max(median_pos * max(1.0 - participation[idx], 0.1), self.gamma_floor)
124
+ raw_alpha = 0.2 + 0.6 * participation[idx] / (1.0 + cv_pos)
125
+ alpha[idx] = float(np.clip(raw_alpha, self.alpha_floor, self.alpha_cap))
126
+
127
+ if self.outside_good is not None and 0 <= self.outside_good < n_alt:
128
+ gamma[self.outside_good] = self.gamma_floor
129
+ alpha[self.outside_good] = max(alpha[self.outside_good], 0.8)
130
+
131
+ self.labels_ = labels_list
132
+ self.baseline_utility_ = baseline
133
+ self.alpha_ = alpha
134
+ self.gamma_ = gamma
135
+ self.fit_result_ = MDCEVFitResult(
136
+ labels=labels_list,
137
+ baseline_utility=baseline.copy(),
138
+ alpha=alpha.copy(),
139
+ gamma=gamma.copy(),
140
+ participation_rate=participation.copy(),
141
+ mean_allocation=mean_allocation.copy(),
142
+ mean_budget=float(np.mean(budgets)),
143
+ )
144
+ return self
145
+
146
+ def fit_mle(
147
+ self,
148
+ allocations,
149
+ labels: Optional[Iterable[str]] = None,
150
+ maxiter: int = 400,
151
+ l2_penalty: float = 1e-4,
152
+ ):
153
+ """Likelihood-based parameter refinement.
154
+
155
+ The objective is a Gaussian log-likelihood on log allocations around
156
+ translated-utility MDCEV deterministic predictions. This is a practical
157
+ quasi-MLE refinement that preserves the MDCEV budget constraint while
158
+ improving fit over pure moments.
159
+ """
160
+ self.fit(allocations, labels=labels)
161
+
162
+ y = _as_2d_float(allocations)
163
+ budgets = y.sum(axis=1)
164
+ n_alt = y.shape[1]
165
+
166
+ free_base_idx = [i for i in range(n_alt) if i != self.outside_good]
167
+
168
+ def _pack(base, alpha, gamma, sigma):
169
+ b = np.asarray(base, dtype=float)
170
+ a = np.asarray(alpha, dtype=float)
171
+ g = np.asarray(gamma, dtype=float)
172
+
173
+ p = []
174
+ p.extend(b[free_base_idx].tolist())
175
+ p.extend(np.log(np.clip((a - self.alpha_floor) / np.clip(self.alpha_cap - a, self.tol, None), self.tol, None)).tolist())
176
+ p.extend(np.log(np.clip(g, self.gamma_floor, None)).tolist())
177
+ p.append(np.log(max(float(sigma), 1e-3)))
178
+ return np.asarray(p, dtype=float)
179
+
180
+ def _unpack(theta):
181
+ theta = np.asarray(theta, dtype=float)
182
+ o = 0
183
+
184
+ base = self.baseline_utility_.copy()
185
+ for idx in free_base_idx:
186
+ base[idx] = theta[o]
187
+ o += 1
188
+ if self.outside_good is not None and 0 <= self.outside_good < n_alt:
189
+ base[self.outside_good] = 0.0
190
+
191
+ alpha_raw = theta[o:o + n_alt]
192
+ o += n_alt
193
+ alpha_sig = 1.0 / (1.0 + np.exp(-alpha_raw))
194
+ alpha = self.alpha_floor + (self.alpha_cap - self.alpha_floor) * alpha_sig
195
+
196
+ gamma_raw = theta[o:o + n_alt]
197
+ o += n_alt
198
+ gamma = np.maximum(np.exp(gamma_raw), self.gamma_floor)
199
+
200
+ sigma = max(np.exp(theta[o]), 1e-3)
201
+ return base, alpha, gamma, sigma
202
+
203
+ def _neg_loglike(theta):
204
+ base, alpha, gamma, sigma = _unpack(theta)
205
+
206
+ old_b, old_a, old_g = self.baseline_utility_, self.alpha_, self.gamma_
207
+ self.baseline_utility_, self.alpha_, self.gamma_ = base, alpha, gamma
208
+ try:
209
+ mu = np.zeros_like(y)
210
+ for i, b in enumerate(budgets):
211
+ mu[i] = self._solve_budget(float(b), base)
212
+ finally:
213
+ self.baseline_utility_, self.alpha_, self.gamma_ = old_b, old_a, old_g
214
+
215
+ log_y = np.log(np.clip(y, self.tol, None))
216
+ log_mu = np.log(np.clip(mu, self.tol, None))
217
+ resid = log_y - log_mu
218
+ ll = -0.5 * resid.size * np.log(2.0 * np.pi * sigma * sigma)
219
+ ll -= 0.5 * np.sum((resid / sigma) ** 2)
220
+ ll -= l2_penalty * np.sum(theta * theta)
221
+ return -float(ll)
222
+
223
+ theta0 = _pack(self.baseline_utility_, self.alpha_, self.gamma_, sigma=0.5)
224
+ res = minimize(
225
+ _neg_loglike,
226
+ theta0,
227
+ method="L-BFGS-B",
228
+ options={"maxiter": int(maxiter), "ftol": 1e-9},
229
+ )
230
+
231
+ base, alpha, gamma, sigma = _unpack(res.x)
232
+ self.baseline_utility_ = base
233
+ self.alpha_ = alpha
234
+ self.gamma_ = gamma
235
+ self.noise_sigma_ = float(sigma)
236
+ self.mle_success_ = bool(res.success)
237
+ self.mle_message_ = str(res.message)
238
+ return self
239
+
240
+ def summary(self) -> pd.DataFrame:
241
+ if self.fit_result_ is None:
242
+ raise RuntimeError("Model must be fit before calling summary()")
243
+ result = self.fit_result_
244
+ return pd.DataFrame(
245
+ {
246
+ "alternative": result.labels,
247
+ "baseline_utility": result.baseline_utility,
248
+ "alpha": result.alpha,
249
+ "gamma": result.gamma,
250
+ "participation_rate": result.participation_rate,
251
+ "mean_allocation": result.mean_allocation,
252
+ }
253
+ )
254
+
255
+ def predict(self, budgets, utility_shift=None) -> np.ndarray:
256
+ """Predict deterministic budget allocations for one or more budgets.
257
+
258
+ Parameters
259
+ ----------
260
+ budgets:
261
+ Scalar or vector of total budgets.
262
+ utility_shift:
263
+ Optional additive utility adjustment. Can be shape ``(J,)`` or
264
+ ``(N, J)``.
265
+ """
266
+ self._check_fitted()
267
+ budgets_arr = np.asarray(budgets, dtype=float).reshape(-1)
268
+ shifts = self._prepare_utility_shift(utility_shift, len(budgets_arr))
269
+
270
+ predictions = np.zeros((len(budgets_arr), len(self.baseline_utility_)), dtype=float)
271
+ for row_idx, budget in enumerate(budgets_arr):
272
+ predictions[row_idx] = self._solve_budget(budget, self.baseline_utility_ + shifts[row_idx])
273
+ return predictions
274
+
275
+ def simulate(self, budgets, utility_shift=None, n_draws: int = 100, random_state: Optional[int] = None) -> np.ndarray:
276
+ """Simulate stochastic budget allocations with Gumbel utility shocks."""
277
+ self._check_fitted()
278
+ budgets_arr = np.asarray(budgets, dtype=float).reshape(-1)
279
+ shifts = self._prepare_utility_shift(utility_shift, len(budgets_arr))
280
+ rng = np.random.default_rng(random_state)
281
+
282
+ sims = np.zeros((n_draws, len(budgets_arr), len(self.baseline_utility_)), dtype=float)
283
+ for draw_idx in range(n_draws):
284
+ shocks = rng.gumbel(loc=0.0, scale=1.0, size=shifts.shape)
285
+ for row_idx, budget in enumerate(budgets_arr):
286
+ sims[draw_idx, row_idx] = self._solve_budget(
287
+ budget,
288
+ self.baseline_utility_ + shifts[row_idx] + shocks[row_idx],
289
+ )
290
+ return sims
291
+
292
+ def _prepare_utility_shift(self, utility_shift, n_rows: int) -> np.ndarray:
293
+ n_alt = len(self.baseline_utility_)
294
+ if utility_shift is None:
295
+ return np.zeros((n_rows, n_alt), dtype=float)
296
+
297
+ shift_arr = np.asarray(utility_shift, dtype=float)
298
+ if shift_arr.ndim == 1:
299
+ if shift_arr.shape[0] != n_alt:
300
+ raise ValueError("utility_shift has the wrong number of alternatives")
301
+ return np.repeat(shift_arr.reshape(1, -1), n_rows, axis=0)
302
+ if shift_arr.shape != (n_rows, n_alt):
303
+ raise ValueError("utility_shift must have shape (J,) or (N, J)")
304
+ return shift_arr
305
+
306
+ def _solve_budget(self, budget: float, utility_index: np.ndarray) -> np.ndarray:
307
+ if budget <= self.tol:
308
+ return np.zeros(len(self.baseline_utility_), dtype=float)
309
+
310
+ weights = np.exp(np.clip(utility_index, -40.0, 40.0))
311
+
312
+ def alloc_for_lambda(lam: float) -> np.ndarray:
313
+ lam = max(lam, self.tol)
314
+ power = 1.0 / np.clip(1.0 - self.alpha_, self.tol, None)
315
+ raw = np.power(weights / lam, power) - self.gamma_
316
+ return np.maximum(raw, 0.0)
317
+
318
+ lo = self.tol
319
+ hi = max(np.max(weights), 1.0)
320
+ while alloc_for_lambda(hi).sum() > budget:
321
+ hi *= 2.0
322
+
323
+ for _ in range(80):
324
+ mid = 0.5 * (lo + hi)
325
+ if alloc_for_lambda(mid).sum() > budget:
326
+ lo = mid
327
+ else:
328
+ hi = mid
329
+
330
+ allocation = alloc_for_lambda(hi)
331
+ total = allocation.sum()
332
+ if total > self.tol:
333
+ allocation *= budget / total
334
+ elif self.outside_good is not None and 0 <= self.outside_good < len(allocation):
335
+ allocation[self.outside_good] = budget
336
+
337
+ residual = budget - allocation.sum()
338
+ if self.outside_good is not None and 0 <= self.outside_good < len(allocation) and residual > self.tol:
339
+ allocation[self.outside_good] += residual
340
+ return allocation
341
+
342
+ def _check_fitted(self):
343
+ if self.fit_result_ is None or self.baseline_utility_ is None:
344
+ raise RuntimeError("Model must be fit before prediction")
@@ -1102,7 +1102,7 @@ class SA(Search):
1102
1102
  # {
1103
1103
  if overall_best_solution is None or \
1104
1104
  is_better(self.best_sol.obj(0), overall_best_solution.obj(0), self.param.sign_crit(0)):
1105
- overall_best_solution = self.best_sol # Update overall best solution
1105
+ overall_best_solution = self.copy_solution(self.best_sol) # Update overall best solution (deep copy to prevent overwriting)
1106
1106
  elif overall_best_solution is not None and \
1107
1107
  is_worse(self.best_sol.obj(0), overall_best_solution.obj(0), self.param.sign_crit(0)):
1108
1108
  self.update_best(overall_best_solution) # Revise best solution of current SA solver
@@ -448,7 +448,7 @@ class TA(Search):
448
448
  # {
449
449
  if overall_best_solution is None or \
450
450
  is_better(self.best_sol.obj(0), overall_best_solution.obj(0), self.param.sign_crit(0)):
451
- overall_best_solution = self.best_sol # Update overall best solution
451
+ overall_best_solution = self.copy_solution(self.best_sol) # Update overall best solution (deep copy to prevent overwriting)
452
452
  elif overall_best_solution is not None and \
453
453
  is_worse(self.best_sol.obj(0), overall_best_solution.obj(0), self.param.sign_crit(0)):
454
454
  self.update_best(overall_best_solution) # Revise best solution of current TA solver
@@ -0,0 +1 @@
1
+ 0.0.85
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SearchLibrium
3
- Version: 0.0.84
3
+ Version: 0.0.85
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>
@@ -17,6 +17,7 @@ src/SearchLibrium/harmony.py
17
17
  src/SearchLibrium/latent_class.py
18
18
  src/SearchLibrium/main.py
19
19
  src/SearchLibrium/main_debug.py
20
+ src/SearchLibrium/mdcev.py
20
21
  src/SearchLibrium/misc.py
21
22
  src/SearchLibrium/mixed_logit.py
22
23
  src/SearchLibrium/mixed_nested.py
@@ -1 +0,0 @@
1
- 0.0.84
File without changes
File without changes