pyepo 2.2.3__tar.gz → 2.2.4__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 (112) hide show
  1. {pyepo-2.2.3 → pyepo-2.2.4}/PKG-INFO +3 -3
  2. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/bases.py +0 -24
  3. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/grb/grbmodel.py +2 -1
  4. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/grb/tsp.py +0 -3
  5. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/grb/vrp.py +0 -3
  6. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/opt.py +41 -7
  7. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo.egg-info/PKG-INFO +3 -3
  8. {pyepo-2.2.3 → pyepo-2.2.4}/pyproject.toml +3 -2
  9. {pyepo-2.2.3 → pyepo-2.2.4}/tests/test_10_utils.py +40 -0
  10. {pyepo-2.2.3 → pyepo-2.2.4}/LICENSE +0 -0
  11. {pyepo-2.2.3 → pyepo-2.2.4}/README.md +0 -0
  12. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/EPO.py +0 -0
  13. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/__init__.py +0 -0
  14. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/data/__init__.py +0 -0
  15. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/data/_validation.py +0 -0
  16. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/data/dataset.py +0 -0
  17. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/data/knapsack.py +0 -0
  18. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/data/portfolio.py +0 -0
  19. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/data/shortestpath.py +0 -0
  20. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/data/tsp.py +0 -0
  21. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/dsl/__init__.py +0 -0
  22. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/dsl/compiled.py +0 -0
  23. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/dsl/expression.py +0 -0
  24. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/dsl/objective.py +0 -0
  25. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/dsl/problem.py +0 -0
  26. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/func/__init__.py +0 -0
  27. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/func/_common.py +0 -0
  28. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/func/abcmodule.py +0 -0
  29. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/func/blackbox.py +0 -0
  30. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/func/cave.py +0 -0
  31. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/func/contrastive.py +0 -0
  32. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/func/jax/__init__.py +0 -0
  33. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/func/jax/abcmodule.py +0 -0
  34. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/func/jax/blackbox.py +0 -0
  35. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/func/jax/cave.py +0 -0
  36. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/func/jax/contrastive.py +0 -0
  37. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/func/jax/perturbed.py +0 -0
  38. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/func/jax/rank.py +0 -0
  39. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/func/jax/regularized.py +0 -0
  40. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/func/jax/surrogate.py +0 -0
  41. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/func/jax/utils.py +0 -0
  42. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/func/perturbed.py +0 -0
  43. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/func/rank.py +0 -0
  44. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/func/regularized.py +0 -0
  45. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/func/runtime.py +0 -0
  46. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/func/surrogate.py +0 -0
  47. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/func/utils.py +0 -0
  48. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/metric/__init__.py +0 -0
  49. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/metric/_common.py +0 -0
  50. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/metric/metrics.py +0 -0
  51. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/metric/mse.py +0 -0
  52. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/metric/regret.py +0 -0
  53. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/metric/unambregret.py +0 -0
  54. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/__init__.py +0 -0
  55. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/_common.py +0 -0
  56. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/copt/__init__.py +0 -0
  57. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/copt/compile.py +0 -0
  58. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/copt/coptmodel.py +0 -0
  59. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/copt/knapsack.py +0 -0
  60. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/copt/portfolio.py +0 -0
  61. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/copt/shortestpath.py +0 -0
  62. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/copt/tsp.py +0 -0
  63. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/copt/vrp.py +0 -0
  64. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/grb/__init__.py +0 -0
  65. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/grb/compile.py +0 -0
  66. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/grb/knapsack.py +0 -0
  67. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/grb/portfolio.py +0 -0
  68. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/grb/shortestpath.py +0 -0
  69. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/mpax/__init__.py +0 -0
  70. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/mpax/compile.py +0 -0
  71. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/mpax/knapsack.py +0 -0
  72. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/mpax/mpaxmodel.py +0 -0
  73. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/mpax/shortestpath.py +0 -0
  74. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/omo/__init__.py +0 -0
  75. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/omo/compile.py +0 -0
  76. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/omo/knapsack.py +0 -0
  77. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/omo/omomodel.py +0 -0
  78. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/omo/portfolio.py +0 -0
  79. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/omo/shortestpath.py +0 -0
  80. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/omo/tsp.py +0 -0
  81. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/omo/vrp.py +0 -0
  82. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/ort/__init__.py +0 -0
  83. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/ort/compile.py +0 -0
  84. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/ort/knapsack.py +0 -0
  85. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/ort/ortcpmodel.py +0 -0
  86. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/ort/ortmodel.py +0 -0
  87. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/ort/shortestpath.py +0 -0
  88. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/predefined.py +0 -0
  89. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/model/utils.py +0 -0
  90. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/py.typed +0 -0
  91. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/twostage/__init__.py +0 -0
  92. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/twostage/autosklearnpred.py +0 -0
  93. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/twostage/sklearnpred.py +0 -0
  94. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo/utils.py +0 -0
  95. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo.egg-info/SOURCES.txt +0 -0
  96. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo.egg-info/dependency_links.txt +0 -0
  97. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo.egg-info/requires.txt +0 -0
  98. {pyepo-2.2.3 → pyepo-2.2.4}/pyepo.egg-info/top_level.txt +0 -0
  99. {pyepo-2.2.3 → pyepo-2.2.4}/setup.cfg +0 -0
  100. {pyepo-2.2.3 → pyepo-2.2.4}/tests/test_00_constants.py +0 -0
  101. {pyepo-2.2.3 → pyepo-2.2.4}/tests/test_15_dsl.py +0 -0
  102. {pyepo-2.2.3 → pyepo-2.2.4}/tests/test_20_data_gen.py +0 -0
  103. {pyepo-2.2.3 → pyepo-2.2.4}/tests/test_30_model.py +0 -0
  104. {pyepo-2.2.3 → pyepo-2.2.4}/tests/test_40_dataset.py +0 -0
  105. {pyepo-2.2.3 → pyepo-2.2.4}/tests/test_50_func.py +0 -0
  106. {pyepo-2.2.3 → pyepo-2.2.4}/tests/test_55_jax.py +0 -0
  107. {pyepo-2.2.3 → pyepo-2.2.4}/tests/test_60_metric.py +0 -0
  108. {pyepo-2.2.3 → pyepo-2.2.4}/tests/test_61_metric_validation.py +0 -0
  109. {pyepo-2.2.3 → pyepo-2.2.4}/tests/test_70_twostage.py +0 -0
  110. {pyepo-2.2.3 → pyepo-2.2.4}/tests/test_80_integration.py +0 -0
  111. {pyepo-2.2.3 → pyepo-2.2.4}/tests/test_85_backend_pipeline.py +0 -0
  112. {pyepo-2.2.3 → pyepo-2.2.4}/tests/test_90_cuda.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyepo
3
- Version: 2.2.3
4
- Summary: PyTorch-based End-to-End Predict-then-Optimize Tool
3
+ Version: 2.2.4
4
+ Summary: PyTorch/JAX-based End-to-End Predict-then-Optimize Tool
5
5
  Author-email: Bo Tang <bolucas.tang@mail.utoronto.ca>
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/khalil-research/PyEPO
@@ -9,7 +9,7 @@ Project-URL: Documentation, https://khalil-research.github.io/PyEPO
9
9
  Project-URL: Repository, https://github.com/khalil-research/PyEPO
10
10
  Project-URL: Issues, https://github.com/khalil-research/PyEPO/issues
11
11
  Project-URL: Paper, https://link.springer.com/article/10.1007/s12532-024-00255-x
12
- Keywords: predict-then-optimize,end-to-end,decision-focused learning,optimization,deep learning,pytorch,linear programming,integer programming
12
+ Keywords: predict-then-optimize,end-to-end,decision-focused learning,optimization,deep learning,pytorch,jax,linear programming,integer programming
13
13
  Classifier: Programming Language :: Python :: 3
14
14
  Classifier: Programming Language :: Python :: 3.9
15
15
  Classifier: Programming Language :: Python :: 3.10
@@ -16,7 +16,6 @@ from __future__ import annotations
16
16
 
17
17
  import math
18
18
  from collections import defaultdict
19
- from copy import deepcopy
20
19
  from itertools import combinations
21
20
  from numbers import Integral, Real
22
21
  from typing import TYPE_CHECKING
@@ -105,9 +104,6 @@ class shortestPathBase(optModel):
105
104
  self.arcs = _get_grid_arcs(self.grid)
106
105
  super().__init__(*args, **kwargs)
107
106
 
108
- def get_config(self) -> dict:
109
- return {**super().get_config(), "grid": self.grid}
110
-
111
107
  @property
112
108
  def num_cost(self) -> int:
113
109
  return len(self.arcs)
@@ -208,14 +204,6 @@ class portfolioBase(optModel):
208
204
  raise ValueError("gamma must be greater than or equal to zero.")
209
205
  super().__init__(*args, **kwargs)
210
206
 
211
- def get_config(self) -> dict:
212
- return {
213
- **super().get_config(),
214
- "num_assets": self.num_assets,
215
- "covariance": self.covariance.copy(),
216
- "gamma": self.gamma,
217
- }
218
-
219
207
  @property
220
208
  def num_cost(self) -> int:
221
209
  return self.num_assets
@@ -263,9 +251,6 @@ class tspABBase(optModel):
263
251
  self._extra_constrs: list = []
264
252
  super().__init__(*args, **kwargs)
265
253
 
266
- def get_config(self) -> dict:
267
- return {**super().get_config(), "num_nodes": self.num_nodes}
268
-
269
254
  @property
270
255
  def num_cost(self) -> int:
271
256
  # use edges; backend's self.x has 2*num_edges directed Vars
@@ -381,15 +366,6 @@ class vrpABBase(optModel):
381
366
  self._extra_constrs: list = []
382
367
  super().__init__(*args, **kwargs)
383
368
 
384
- def get_config(self) -> dict:
385
- return {
386
- **super().get_config(),
387
- "num_nodes": self.num_nodes,
388
- "demands": deepcopy(self.demands),
389
- "capacity": self.capacity,
390
- "num_vehicle": self.num_vehicle,
391
- }
392
-
393
369
  @property
394
370
  def num_cost(self) -> int:
395
371
  # one predicted cost per undirected edge
@@ -171,7 +171,8 @@ class optGrbModel(optModel):
171
171
  else:
172
172
  # LinExpr(coeffs, vars) builds the affine expression in one C call
173
173
  vars_list = new_model._vars_list
174
- assert vars_list is not None
174
+ if vars_list is None:
175
+ raise RuntimeError("Gurobi variable list is unavailable.")
175
176
  expr = gp.LinExpr(coefs_np.tolist(), vars_list) <= rhs
176
177
  new_model._model.addConstr(expr)
177
178
  # track for replay on relax
@@ -178,9 +178,6 @@ class tspDFJModel(tspABModel):
178
178
  self._recycled_keys: set = set()
179
179
  super().__init__(num_nodes, *args, **kwargs)
180
180
 
181
- def get_config(self) -> dict:
182
- return {**super().get_config(), "recycle_cuts": self.recycle_cuts}
183
-
184
181
  def _getModel(self) -> tuple:
185
182
  """
186
183
  A method to build Gurobi model
@@ -70,9 +70,6 @@ class vrpRCIModel(vrpABModel):
70
70
  self._recycled_keys: set = set()
71
71
  super().__init__(num_nodes, demands, capacity, num_vehicle)
72
72
 
73
- def get_config(self) -> dict:
74
- return {**super().get_config(), "recycle_cuts": self.recycle_cuts}
75
-
76
73
  def _getModel(self) -> tuple:
77
74
  """
78
75
  A method to build Gurobi model
@@ -5,6 +5,8 @@ Abstract optimization model
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
+ import functools
9
+ import inspect
8
10
  from abc import ABC, abstractmethod
9
11
  from copy import deepcopy
10
12
  from dataclasses import dataclass
@@ -41,6 +43,27 @@ class ModelSpec:
41
43
  return self.model_type.from_config(self._config)
42
44
 
43
45
 
46
+ def _capture_init_config(init, args, kwargs) -> dict:
47
+ """Flatten a constructor call into keyword arguments that rebuild the model."""
48
+ sig = inspect.signature(init)
49
+ bound = sig.bind(None, *args, **kwargs)
50
+ config = {}
51
+ for i, (name, value) in enumerate(bound.arguments.items()):
52
+ # the first bound argument is self
53
+ if i == 0:
54
+ continue
55
+ kind = sig.parameters[name].kind
56
+ # **kwargs: merge captured keywords in directly
57
+ if kind is inspect.Parameter.VAR_KEYWORD:
58
+ config.update(deepcopy(value))
59
+ # nameless positionals cannot replay by keyword; override get_config to keep them
60
+ elif kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.POSITIONAL_ONLY):
61
+ continue
62
+ else:
63
+ config[name] = deepcopy(value)
64
+ return config
65
+
66
+
44
67
  class optModel(ABC):
45
68
  """
46
69
  Abstract base class for predict-then-optimize models.
@@ -53,11 +76,6 @@ class optModel(ABC):
53
76
  and MPAX (``optMpaxModel``); subclass ``optModel`` directly to integrate
54
77
  any other solver or algorithm.
55
78
 
56
- Models that take constructor arguments should override ``get_config`` and
57
- cooperatively merge ``super().get_config()``. The resulting configuration
58
- powers ``rebuild()``, multiprocessing workers, and sklearn scorers without
59
- inspecting constructor signatures or runtime solver state.
60
-
61
79
  The default objective sense is minimization; set
62
80
  ``self.modelSense = EPO.MAXIMIZE`` in ``_getModel`` or ``__init__`` for
63
81
  maximization problems (some backends, e.g. Gurobi and COPT, detect this
@@ -73,6 +91,22 @@ class optModel(ABC):
73
91
  arcs: list
74
92
  _cost_vars: list
75
93
 
94
+ def __init_subclass__(cls, **kwargs) -> None:
95
+ super().__init_subclass__(**kwargs)
96
+ # only wrap a subclass that defines its own __init__
97
+ if "__init__" not in cls.__dict__:
98
+ return
99
+ user_init = cls.__init__
100
+
101
+ @functools.wraps(user_init)
102
+ def _init_capturing(self, *args, **kwargs):
103
+ # record only the outermost call; nested super().__init__ leaves it intact
104
+ if "_init_config" not in self.__dict__:
105
+ self._init_config = _capture_init_config(user_init, args, kwargs)
106
+ user_init(self, *args, **kwargs)
107
+
108
+ cls.__init__ = _init_capturing
109
+
76
110
  def __init__(self) -> None:
77
111
  # Cache for models whose solver variables do not map one-to-one to
78
112
  # predicted costs (for example directed TSP/VRP formulations).
@@ -86,8 +120,8 @@ class optModel(ABC):
86
120
  return "optModel " + self.__class__.__name__
87
121
 
88
122
  def get_config(self) -> dict:
89
- """Return the explicit constructor configuration for this model."""
90
- return {}
123
+ """Return the constructor configuration for this model."""
124
+ return deepcopy(self.__dict__.get("_init_config", {}))
91
125
 
92
126
  @classmethod
93
127
  def from_config(cls, config: dict) -> Self:
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyepo
3
- Version: 2.2.3
4
- Summary: PyTorch-based End-to-End Predict-then-Optimize Tool
3
+ Version: 2.2.4
4
+ Summary: PyTorch/JAX-based End-to-End Predict-then-Optimize Tool
5
5
  Author-email: Bo Tang <bolucas.tang@mail.utoronto.ca>
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/khalil-research/PyEPO
@@ -9,7 +9,7 @@ Project-URL: Documentation, https://khalil-research.github.io/PyEPO
9
9
  Project-URL: Repository, https://github.com/khalil-research/PyEPO
10
10
  Project-URL: Issues, https://github.com/khalil-research/PyEPO/issues
11
11
  Project-URL: Paper, https://link.springer.com/article/10.1007/s12532-024-00255-x
12
- Keywords: predict-then-optimize,end-to-end,decision-focused learning,optimization,deep learning,pytorch,linear programming,integer programming
12
+ Keywords: predict-then-optimize,end-to-end,decision-focused learning,optimization,deep learning,pytorch,jax,linear programming,integer programming
13
13
  Classifier: Programming Language :: Python :: 3
14
14
  Classifier: Programming Language :: Python :: 3.9
15
15
  Classifier: Programming Language :: Python :: 3.10
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyepo"
7
- version = "2.2.3"
8
- description = "PyTorch-based End-to-End Predict-then-Optimize Tool"
7
+ version = "2.2.4"
8
+ description = "PyTorch/JAX-based End-to-End Predict-then-Optimize Tool"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  license = { text = "MIT" }
11
11
  authors = [{ name = "Bo Tang", email = "bolucas.tang@mail.utoronto.ca" }]
@@ -17,6 +17,7 @@ keywords = [
17
17
  "optimization",
18
18
  "deep learning",
19
19
  "pytorch",
20
+ "jax",
20
21
  "linear programming",
21
22
  "integer programming",
22
23
  ]
@@ -43,6 +43,24 @@ class ConfigModel(optModel):
43
43
  return np.zeros(len(self.values)), 0.0
44
44
 
45
45
 
46
+ class AutoConfigModel(optModel):
47
+ """Solver-free model that relies on optModel's automatic config capture."""
48
+
49
+ def __init__(self, values, **kwargs):
50
+ self.values = values
51
+ self.kwargs = kwargs
52
+ super().__init__()
53
+
54
+ def _getModel(self):
55
+ return None, list(range(len(self.values)))
56
+
57
+ def setObj(self, c):
58
+ self.cost = np.asarray(c)
59
+
60
+ def solve(self):
61
+ return np.zeros(len(self.values)), 0.0
62
+
63
+
46
64
  # ============================================================
47
65
  # unionFind (pure)
48
66
  # ============================================================
@@ -222,6 +240,28 @@ class TestModelSpec:
222
240
  np.testing.assert_array_equal(sol, [0.0, 0.0, 0.0])
223
241
  assert obj == 0.0
224
242
 
243
+ def test_auto_config_snapshots_constructor_inputs(self):
244
+ values = [1, 2, 3]
245
+ nested = {"tag": ["x"]}
246
+ model = AutoConfigModel(values, nested=nested)
247
+ values[0] = 99
248
+ nested["tag"][0] = "changed"
249
+
250
+ rebuilt = model.rebuild()
251
+
252
+ assert rebuilt.values == [1, 2, 3]
253
+ assert rebuilt.kwargs == {"nested": {"tag": ["x"]}}
254
+
255
+ def test_auto_config_export_is_independent(self):
256
+ model = AutoConfigModel([1, 2, 3], nested={"tag": ["x"]})
257
+ config = model.get_config()
258
+ config["values"][0] = 99
259
+ config["nested"]["tag"][0] = "changed"
260
+
261
+ assert model.get_config()["values"] == [1, 2, 3]
262
+ assert model.get_config()["nested"] == {"tag": ["x"]}
263
+ assert model.rebuild().kwargs == {"nested": {"tag": ["x"]}}
264
+
225
265
 
226
266
  # ============================================================
227
267
  # getArgs (needs a real optModel)
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
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