eqc-models 0.14.4__py3-none-any.whl → 0.15.0__py3-none-any.whl

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 (74) hide show
  1. eqc_models-0.15.0.data/platlib/eqc_models/algorithms/__init__.py +12 -0
  2. eqc_models-0.15.0.data/platlib/eqc_models/algorithms/alm.py +464 -0
  3. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/base/polyeval.c +4986 -4919
  4. eqc_models-0.15.0.data/platlib/eqc_models/base/polyeval.cpython-310-darwin.so +0 -0
  5. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/graph/rcshortestpath.py +1 -1
  6. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/graph/shortestpath.py +26 -4
  7. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/solvers/__init__.py +2 -2
  8. {eqc_models-0.14.4.dist-info → eqc_models-0.15.0.dist-info}/METADATA +1 -1
  9. eqc_models-0.15.0.dist-info/RECORD +71 -0
  10. eqc_models-0.14.4.data/platlib/eqc_models/algorithms/__init__.py +0 -4
  11. eqc_models-0.14.4.data/platlib/eqc_models/base/polyeval.cpython-310-darwin.so +0 -0
  12. eqc_models-0.14.4.dist-info/RECORD +0 -70
  13. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/compile_extensions.py +0 -0
  14. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/__init__.py +0 -0
  15. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/algorithms/base.py +0 -0
  16. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/algorithms/penaltymultiplier.py +0 -0
  17. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/allocation/__init__.py +0 -0
  18. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/allocation/allocation.py +0 -0
  19. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/allocation/portbase.py +0 -0
  20. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/allocation/portmomentum.py +0 -0
  21. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/assignment/__init__.py +0 -0
  22. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/assignment/qap.py +0 -0
  23. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/assignment/resource.py +0 -0
  24. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/assignment/setpartition.py +0 -0
  25. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/base/__init__.py +0 -0
  26. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/base/base.py +0 -0
  27. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/base/binaries.py +0 -0
  28. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/base/constraints.py +0 -0
  29. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/base/operators.py +0 -0
  30. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/base/polyeval.pyx +0 -0
  31. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/base/polynomial.py +0 -0
  32. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/base/quadratic.py +0 -0
  33. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/base/results.py +0 -0
  34. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/combinatorics/__init__.py +0 -0
  35. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/combinatorics/setcover.py +0 -0
  36. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/combinatorics/setpartition.py +0 -0
  37. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/decoding.py +0 -0
  38. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/graph/__init__.py +0 -0
  39. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/graph/base.py +0 -0
  40. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/graph/hypergraph.py +0 -0
  41. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/graph/maxcut.py +0 -0
  42. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/graph/maxkcut.py +0 -0
  43. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/graph/partition.py +0 -0
  44. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/ml/__init__.py +0 -0
  45. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/ml/classifierbase.py +0 -0
  46. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/ml/classifierqboost.py +0 -0
  47. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/ml/classifierqsvm.py +0 -0
  48. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/ml/clustering.py +0 -0
  49. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/ml/clusteringbase.py +0 -0
  50. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/ml/cvqboost_hamiltonian.pyx +0 -0
  51. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/ml/cvqboost_hamiltonian_c_func.c +0 -0
  52. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/ml/cvqboost_hamiltonian_c_func.h +0 -0
  53. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/ml/decomposition.py +0 -0
  54. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/ml/forecast.py +0 -0
  55. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/ml/forecastbase.py +0 -0
  56. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/ml/regressor.py +0 -0
  57. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/ml/regressorbase.py +0 -0
  58. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/ml/reservoir.py +0 -0
  59. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/process/base.py +0 -0
  60. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/process/mpc.py +0 -0
  61. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/sequence/__init__.py +0 -0
  62. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/sequence/tsp.py +0 -0
  63. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/solvers/eqcdirect.py +0 -0
  64. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/solvers/mip.py +0 -0
  65. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/solvers/qciclient.py +0 -0
  66. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/solvers/responselog.py +0 -0
  67. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/utilities/__init__.py +0 -0
  68. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/utilities/fileio.py +0 -0
  69. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/utilities/general.py +0 -0
  70. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/utilities/polynomial.py +0 -0
  71. {eqc_models-0.14.4.data → eqc_models-0.15.0.data}/platlib/eqc_models/utilities/qplib.py +0 -0
  72. {eqc_models-0.14.4.dist-info → eqc_models-0.15.0.dist-info}/WHEEL +0 -0
  73. {eqc_models-0.14.4.dist-info → eqc_models-0.15.0.dist-info}/licenses/LICENSE.txt +0 -0
  74. {eqc_models-0.14.4.dist-info → eqc_models-0.15.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,12 @@
1
+ # (C) Quantum Computing Inc., 2024.
2
+ from .penaltymultiplier import PenaltyMultiplierAlgorithm
3
+ from .alm import (
4
+ ALMAlgorithm,
5
+ ConstraintRegistry,
6
+ ALMConfig,
7
+ ALMConstraint,
8
+ ALMBlock
9
+ )
10
+
11
+ __all__ = ["PenaltyMultiplierAlgorithm", "ALMAlgorithm", "ConstraintRegistry",
12
+ "ALMConfig", "ALMConstraint", "ALMBlock",]
@@ -0,0 +1,464 @@
1
+ # (C) Quantum Computing Inc., 2025.
2
+ from dataclasses import dataclass
3
+ from typing import Callable, Dict, List, Tuple, Optional, Sequence, Union
4
+ import numpy as np
5
+ from eqc_models.base.polynomial import PolynomialModel
6
+
7
+ Array = np.ndarray
8
+ PolyTerm = Tuple[Tuple[int, ...], float]
9
+
10
+
11
+ @dataclass
12
+ class ALMConstraint:
13
+ """One constraint family; fun returns a vector; jac returns its Jacobian."""
14
+ kind: str # "eq" or "ineq"
15
+ fun: Callable[[Array], Array] # h(x) or g(x)
16
+ jac: Optional[Callable[[Array], Array]] = None
17
+ name: str = ""
18
+
19
+
20
+ @dataclass
21
+ class ALMBlock:
22
+ """Lifted discrete variable block (optional)."""
23
+ idx: Sequence[int] # indices of block in the full x
24
+ levels: Array # (k,) level values (b_i)
25
+ enforce_sum_to_one: bool = True # register as equality via helper
26
+ enforce_one_hot: bool = True # ALM linearization with M = 11^T - I
27
+
28
+
29
+ @dataclass
30
+ class ALMConfig:
31
+ # penalties
32
+ rho_h: float = 50.0 # equalities
33
+ rho_g: float = 50.0 # inequalities / one-hot
34
+ rho_min: float = 1e-3
35
+ rho_max: float = 1e3
36
+ # adaptation toggles
37
+ adapt: bool = True
38
+ tau_up_h: float = 0.90
39
+ tau_down_h: float = 0.50
40
+ tau_up_g: float = 0.90
41
+ tau_down_g: float = 0.50
42
+ gamma_up: float = 2.0
43
+ gamma_down: float = 1.0
44
+ # tolerances & loop
45
+ tol_h: float = 1e-6
46
+ tol_g: float = 1e-6
47
+ max_outer: int = 100
48
+ # stagnation safety net
49
+ use_stagnation_bump: bool = True
50
+ patience_h: int = 10
51
+ patience_g: int = 10
52
+ stagnation_factor: float = 1e-3
53
+ # smoothing (optional)
54
+ ema_alpha: float = 0.3
55
+ # finite diff (only used if jac=None)
56
+ fd_eps: float = 1e-6
57
+ # activation threshold for projected ALM
58
+ act_tol: float = 1e-10
59
+
60
+
61
+ class ConstraintRegistry:
62
+ """
63
+ Holds constraints and block metadata; keeps ALMAlgorithm stateless. Register constraints and
64
+ (optional) lifted-discrete blocks here.
65
+ """
66
+ def __init__(self):
67
+ self.constraints: List[ALMConstraint] = []
68
+ self.blocks: List[ALMBlock] = []
69
+
70
+ def add_equality(self, fun, jac=None, name=""):
71
+ self.constraints.append(ALMConstraint("eq", fun, jac, name))
72
+
73
+ def add_inequality(self, fun, jac=None, name=""):
74
+ self.constraints.append(ALMConstraint("ineq", fun, jac, name))
75
+
76
+ def add_block(self, idx: Sequence[int], levels: Array, sum_to_one=True, one_hot=True):
77
+ self.blocks.append(ALMBlock(list(idx), np.asarray(levels, float), sum_to_one, one_hot))
78
+
79
+
80
+ class ALMAlgorithm:
81
+ """Stateless ALM outer loop. Call `run(model, registry, core, cfg, **core_kwargs)`."""
82
+
83
+ # ---- helpers (static) ----
84
+ @staticmethod
85
+ def _finite_diff_jac(fun: Callable[[Array], Array], x: Array, eps: float) -> Array:
86
+ y0 = fun(x)
87
+ m = int(np.prod(y0.shape))
88
+ y0 = y0.reshape(-1)
89
+ n = x.size
90
+ J = np.zeros((m, n), dtype=float)
91
+ for j in range(n):
92
+ xp = x.copy()
93
+ xp[j] += eps
94
+ J[:, j] = (fun(xp).reshape(-1) - y0) / eps
95
+ return J
96
+
97
+ @staticmethod
98
+ def _pairwise_M(k: int) -> Array:
99
+ return np.ones((k, k), dtype=float) - np.eye(k, dtype=float)
100
+
101
+ @staticmethod
102
+ def _sum_to_one_selector(n: int, idx: Sequence[int]) -> Array:
103
+ S = np.zeros((1, n), dtype=float)
104
+ S[0, np.array(list(idx), int)] = 1.0
105
+ return S
106
+
107
+ @staticmethod
108
+ def _make_sum1_fun(S):
109
+ return lambda x: S @ x - np.array([1.0])
110
+
111
+ @staticmethod
112
+ def _make_sum1_jac(S):
113
+ return lambda x: S
114
+
115
+ @staticmethod
116
+ def _make_onehot_fun(sl, M):
117
+ sl = np.array(sl, int)
118
+
119
+ def _f(x):
120
+ s = x[sl]
121
+ return np.array([float(s @ (M @ s))]) # shape (1,)
122
+
123
+ return _f
124
+
125
+ @staticmethod
126
+ def _make_onehot_jac(sl, M, n):
127
+ sl = np.array(sl, int)
128
+
129
+ def _J(x):
130
+ s = x[sl]
131
+ grad_blk = 2.0 * (M @ s) # (k,)
132
+ J = np.zeros((1, n), dtype=float) # shape (1, n)
133
+ J[0, sl] = grad_blk
134
+ return J
135
+
136
+ return _J
137
+
138
+ @staticmethod
139
+ def _poly_value(poly_terms: List[PolyTerm], x: Array) -> float:
140
+ val = 0.0
141
+ for inds, coeff in poly_terms:
142
+ prod = 1.0
143
+ for j in inds:
144
+ if j == 0:
145
+ continue
146
+ else:
147
+ prod *= x[j - 1]
148
+ val += coeff * prod
149
+ return float(val)
150
+
151
+ @staticmethod
152
+ def _merge_poly(poly_terms: Optional[List[PolyTerm]], Q_aug: Optional[Array],
153
+ c_aug: Optional[Array]) -> List[PolyTerm]:
154
+ """
155
+ Merge ALM's quadratic/linear increments (Q_aug, c_aug) into the base polynomial term list `poly_terms`.
156
+ If 'poly_terms' is None, then turn x^T Q_aug x + c_aug^T x into polynomial monomials.
157
+ Terms are of the form:
158
+ ((0, i), w) for linear, ((i, j), w) for quadratic.
159
+ """
160
+ merged = list(poly_terms) if poly_terms is not None else []
161
+
162
+ if Q_aug is not None:
163
+ Qs = 0.5 * (Q_aug + Q_aug.T)
164
+ n = Qs.shape[0]
165
+ for i in range(n):
166
+ # diagonal contributes Qii * x_i^2
167
+ if Qs[i, i] != 0.0:
168
+ merged.append(((i + 1, i + 1), float(Qs[i, i])))
169
+ for j in range(i + 1, n):
170
+ q = 2.0 * Qs[i, j] # x^T Q x -> sum_{i<j} 2*Q_ij x_i x_j
171
+ if q != 0.0:
172
+ merged.append(((i + 1, j + 1), float(q)))
173
+ if c_aug is not None:
174
+ for i, ci in enumerate(c_aug):
175
+ if ci != 0.0:
176
+ merged.append(((0, i + 1), float(ci)))
177
+ return merged
178
+
179
+ # ---- main entrypoint ----
180
+ @staticmethod
181
+ def run(
182
+ model: PolynomialModel,
183
+ registry: ConstraintRegistry,
184
+ solver,
185
+ cfg: ALMConfig = ALMConfig(),
186
+ x0: Optional[Array] = None,
187
+ *,
188
+ parse_output=None,
189
+ verbose: bool = True,
190
+ **solver_kwargs,
191
+ ) -> Dict[str, Union[Array, Dict[int, float], Dict]]:
192
+ """
193
+ Solve with ALM. Keep all ALM state local to this call (no global side-effects).
194
+
195
+ Returns:
196
+ {
197
+ "x": final iterate,
198
+ "decoded": {start_idx_of_block: level_value, ...} for lifted blocks,
199
+ "hist": { "eq_inf": [...], "ineq_inf": [...], "obj": [...], "x": [...] }
200
+ }
201
+ """
202
+ n = int(getattr(model, "n", len(getattr(model, "upper_bound", [])) or 0))
203
+ x = (np.asarray(x0, float).copy() if x0 is not None else
204
+ np.zeros(n, float))
205
+ lb = getattr(model, "lower_bound", None)
206
+ ub = getattr(model, "upper_bound", None)
207
+
208
+ # ---- collect constraints ----
209
+ problem_eqs = [c for c in registry.constraints if c.kind == "eq"]
210
+ problem_ineqs = [c for c in registry.constraints if c.kind == "ineq"]
211
+
212
+ # auto-install sum-to-one and one-hot as equalities
213
+ # (One-hot: s^T (11^T - I) s = 0))
214
+ def _install_block_equalities() -> List[ALMConstraint]:
215
+ eqs: List[ALMConstraint] = []
216
+ for blk in registry.blocks:
217
+ if blk.enforce_sum_to_one:
218
+ S = ALMAlgorithm._sum_to_one_selector(n, blk.idx)
219
+ eqs.append(ALMConstraint(
220
+ "eq",
221
+ fun=ALMAlgorithm._make_sum1_fun(S),
222
+ jac=ALMAlgorithm._make_sum1_jac(S),
223
+ name=f"sum_to_one_block_{blk.idx[0]}",
224
+ ))
225
+ if blk.enforce_one_hot:
226
+ k = len(blk.idx)
227
+ M = ALMAlgorithm._pairwise_M(k)
228
+ eqs.append(ALMConstraint(
229
+ "eq",
230
+ fun=ALMAlgorithm._make_onehot_fun(blk.idx, M),
231
+ jac=ALMAlgorithm._make_onehot_jac(blk.idx, M, n),
232
+ name=f"onehot_block_{blk.idx[0]}",
233
+ ))
234
+ return eqs
235
+
236
+ block_eqs = _install_block_equalities()
237
+
238
+ # Unified equality list (order is fixed for whole run)
239
+ full_eqs = problem_eqs + block_eqs
240
+
241
+ # Allocate multipliers for every equality in full_eqs
242
+ lam_eq = []
243
+ for csp in full_eqs:
244
+ r0 = csp.fun(x).reshape(-1)
245
+ lam_eq.append(np.zeros_like(r0, dtype=float))
246
+
247
+ # Inequality multipliers per user inequality
248
+ mu_ineq = []
249
+ for csp in problem_ineqs:
250
+ r0 = csp.fun(x).reshape(-1)
251
+ mu_ineq.append(np.zeros_like(r0, dtype=float))
252
+
253
+ # -------- running stats for adaptive penalties --------
254
+ rho_h, rho_g = cfg.rho_h, cfg.rho_g
255
+ best_eq, best_ineq = np.inf, np.inf
256
+ no_imp_eq = no_imp_ineq = 0
257
+ prev_eq_inf, prev_ineq_inf = np.inf, np.inf
258
+ eps = 1e-12
259
+
260
+ hist = {"eq_inf": [], "ineq_inf": [], "obj": [], "x": [],
261
+ # per-iteration logs for parameters/multipliers
262
+ "rho_h": [], "rho_g": [],
263
+ }
264
+ for k_idx, csp in enumerate(full_eqs):
265
+ if csp.kind != "eq":
266
+ continue
267
+ hist[f"lam_eq_max_idx{k_idx}"] = []
268
+ hist[f"lam_eq_min_idx{k_idx}"] = []
269
+ for k_idx, csp in enumerate(problem_ineqs):
270
+ if csp.kind != "ineq":
271
+ continue
272
+ hist[f"mu_ineq_max_idx{k_idx}"] = []
273
+ hist[f"mu_ineq_min_idx{k_idx}"] = []
274
+
275
+ for it in range(cfg.max_outer):
276
+ # -------- base polynomial (does not include fixed penalties here) --------
277
+ # base_terms: List[PolyTerm] = list(getattr(model, "polynomial"))
278
+ base_terms: List[PolyTerm] = list(zip(model.polynomial.indices, model.polynomial.coefficients))
279
+
280
+ # -------- ALM quadratic/linear pieces (assembled here, kept separate) --------
281
+ Q_aug = np.zeros((n, n), dtype=float)
282
+ c_aug = np.zeros(n, dtype=float)
283
+ have_aug = False
284
+
285
+ # (A) Equalities: linearize h near x^t => (rho/2)||A x - b||^2 + lam^T(Ax - b)
286
+ for k_idx, csp in enumerate(full_eqs):
287
+ if csp.kind != "eq":
288
+ continue
289
+ h = csp.fun(x).reshape(-1)
290
+ A = csp.jac(x) if csp.jac is not None else ALMAlgorithm._finite_diff_jac(csp.fun, x, cfg.fd_eps)
291
+ A = np.atleast_2d(A)
292
+ assert A.shape[1] == n, f"A has {A.shape[1]} cols, expected {n}"
293
+ # linearization about current x: residual model r(x) = A x - b, with b = A x - h
294
+ b = A @ x - h
295
+ Qk = 0.5 * rho_h * (A.T @ A)
296
+ ck = (A.T @ lam_eq[k_idx]) - rho_h * (A.T @ b)
297
+ Q_aug += Qk
298
+ c_aug += ck
299
+ have_aug = True
300
+
301
+ # (B) Inequalities: projected ALM. Linearize g near x^t.
302
+ for k_idx, csp in enumerate(problem_ineqs):
303
+ if csp.kind != "ineq":
304
+ continue
305
+ g = csp.fun(x).reshape(-1)
306
+ G = csp.jac(x) if csp.jac is not None else ALMAlgorithm._finite_diff_jac(csp.fun, x, cfg.fd_eps)
307
+ G = np.atleast_2d(G)
308
+ assert G.shape[1] == n, f"G has {G.shape[1]} cols, expected {n}"
309
+ d = G @ x - g
310
+ # Activation measure at current iterate; meaning, the current violating inequality components:
311
+ # g(x) + mu/rho; Powell-Hestenes-Rockafellar shifted residual
312
+ y = G @ x - d + mu_ineq[k_idx] / rho_g
313
+ active = (y > cfg.act_tol)
314
+ if np.any(active):
315
+ GA = G[active, :]
316
+ muA = mu_ineq[k_idx][active]
317
+ gA = g[active]
318
+ # Q += (rho/2) * GA^T GA
319
+ Qk = 0.5 * rho_g * (GA.T @ GA)
320
+ # c += GA^T mu - rho * GA^T (GA x - gA); where GA x - gA is active measures of d = G @ x - g
321
+ ck = (GA.T @ muA) - rho_g * (GA.T @ (GA @ x - gA))
322
+ Q_aug += Qk
323
+ c_aug += ck
324
+ have_aug = True
325
+
326
+ # -------- build merged polynomial for the core solver --------
327
+ all_terms = ALMAlgorithm._merge_poly(base_terms, Q_aug if have_aug else None,
328
+ c_aug if have_aug else None)
329
+ idxs, coeffs = zip(*[(inds, w) for (inds, w) in all_terms]) if all_terms else ([], [])
330
+ poly_model = PolynomialModel(list(coeffs), list(idxs))
331
+ if lb is not None and hasattr(poly_model, "lower_bound"):
332
+ poly_model.lower_bound = np.asarray(lb, float)
333
+ if ub is not None and hasattr(poly_model, "upper_bound"):
334
+ poly_model.upper_bound = np.asarray(ub, float)
335
+
336
+ x_ws = x.copy()
337
+
338
+ # Convention: many cores look for one of these fields if present.
339
+ # Use one or more to be future-proof; harmless if ignored.
340
+ setattr(poly_model, "initial_guess", x_ws)
341
+ setattr(poly_model, "warm_start", x_ws)
342
+ setattr(poly_model, "x0", x_ws)
343
+
344
+ # -------- inner solve --------
345
+ out = solver.solve(poly_model, **solver_kwargs)
346
+
347
+ # -------- parse --------
348
+ if parse_output:
349
+ x = parse_output(out)
350
+ else:
351
+ # default: support (value, x) or `.x` or raw x
352
+ if isinstance(out, tuple) and len(out) == 2:
353
+ _, x = out
354
+ elif isinstance(out, dict) and "results" in out and "solutions" in out["results"]:
355
+ x = out["results"]["solutions"][0]
356
+ elif isinstance(out, dict) and "x" in out:
357
+ x = out["x"]
358
+ else:
359
+ x = getattr(out, "x", out)
360
+ x = np.asarray(x, float)
361
+
362
+ # -------- residuals + multiplier updates --------
363
+ eq_infs = []
364
+ for k_idx, csp in enumerate(full_eqs):
365
+ if csp.kind != "eq": continue
366
+ r = csp.fun(x).reshape(-1)
367
+ lam_eq[k_idx] = lam_eq[k_idx] + rho_h * r
368
+ if r.size:
369
+ eq_infs.append(np.max(np.abs(r)))
370
+ eq_inf = float(np.max(eq_infs)) if eq_infs else 0.0
371
+
372
+ ineq_infs = []
373
+ for k_idx, csp in enumerate(problem_ineqs):
374
+ if csp.kind != "ineq": continue
375
+ r = csp.fun(x).reshape(-1)
376
+ mu_ineq[k_idx] = np.maximum(0.0, mu_ineq[k_idx] + rho_g * r)
377
+ if r.size:
378
+ ineq_infs.append(np.max(np.maximum(0.0, r)))
379
+ ineq_inf = float(np.max(ineq_infs)) if ineq_infs else 0.0
380
+
381
+ assert len(lam_eq) == len(full_eqs)
382
+ assert len(mu_ineq) == len(problem_ineqs)
383
+
384
+ # evaluate base polynomial only (ca add aug value if want to track full L_A)
385
+ f_val = ALMAlgorithm._poly_value(base_terms, x)
386
+
387
+ hist["eq_inf"].append(eq_inf); hist["ineq_inf"].append(ineq_inf)
388
+ hist["obj"].append(float(f_val)); hist["x"].append(x.copy())
389
+ # parameter & multiplier tracking
390
+ hist["rho_h"].append(float(rho_h)); hist["rho_g"].append(float(rho_g))
391
+ for k_idx, csp in enumerate(full_eqs):
392
+ if csp.kind != "eq": continue
393
+ hist[f"lam_eq_max_idx{k_idx}"].append(float(np.max(lam_eq[k_idx])))
394
+ hist[f"lam_eq_min_idx{k_idx}"].append(float(np.min(lam_eq[k_idx])))
395
+ for k_idx, csp in enumerate(problem_ineqs):
396
+ if csp.kind != "ineq": continue
397
+ hist[f"mu_ineq_max_idx{k_idx}"].append(float(np.max(mu_ineq[k_idx])))
398
+ hist[f"mu_ineq_min_idx{k_idx}"].append(float(np.min(mu_ineq[k_idx])))
399
+
400
+ if verbose:
401
+ print(f"[ALM {it:02d}] f={f_val:.6g} | eq_inf={eq_inf:.2e} | ineq_inf={ineq_inf:.2e} "
402
+ f"| rho_h={rho_h:.2e} | rho_g={rho_g:.2e}")
403
+
404
+ # stopping
405
+ if eq_inf <= cfg.tol_h and ineq_inf <= cfg.tol_g:
406
+ if verbose:
407
+ print(f"[ALM] converged at iter {it}")
408
+ break
409
+
410
+ # EMA smoothing to reduce jitter
411
+ if it == 0:
412
+ eq_inf_smooth = eq_inf
413
+ ineq_inf_smooth = ineq_inf
414
+ else:
415
+ eq_inf_smooth = cfg.ema_alpha * eq_inf + (1 - cfg.ema_alpha) * eq_inf_smooth
416
+ ineq_inf_smooth = cfg.ema_alpha * ineq_inf + (1 - cfg.ema_alpha) * ineq_inf_smooth
417
+
418
+ # -------- Residual-ratio controller --------
419
+ if cfg.adapt and it > 0:
420
+ # Equality group
421
+ if eq_inf_smooth > cfg.tau_up_h * max(prev_eq_inf, eps): # stalled or not shrinking
422
+ rho_h = min(cfg.gamma_up * rho_h, cfg.rho_max)
423
+ elif eq_inf_smooth < cfg.tau_down_h * max(prev_eq_inf, eps): # fast progress, allow relaxation
424
+ rho_h = max(cfg.gamma_down * rho_h, cfg.rho_min)
425
+
426
+ # Inequality group
427
+ if ineq_inf_smooth > cfg.tau_up_g * max(prev_ineq_inf, eps):
428
+ rho_g = min(cfg.gamma_up * rho_g, cfg.rho_max)
429
+ elif ineq_inf_smooth < cfg.tau_down_g * max(prev_ineq_inf, eps):
430
+ rho_g = max(cfg.gamma_down * rho_g, cfg.rho_min)
431
+
432
+ # -------- Stagnation bump (safety net) --------
433
+ if cfg.use_stagnation_bump:
434
+ # Equality stagnation
435
+ if eq_inf <= best_eq * (1 - cfg.stagnation_factor):
436
+ best_eq = eq_inf; no_imp_eq = 0
437
+ else:
438
+ no_imp_eq += 1
439
+ if no_imp_eq >= cfg.patience_h:
440
+ rho_h = min(2.0 * rho_h, cfg.rho_max); no_imp_eq = 0
441
+
442
+ # Inequality stagnation
443
+ if ineq_inf <= best_ineq * (1 - cfg.stagnation_factor):
444
+ best_ineq = ineq_inf; no_imp_ineq = 0
445
+ else:
446
+ no_imp_ineq += 1
447
+ if no_imp_ineq >= cfg.patience_g:
448
+ rho_g = min(2.0 * rho_g, cfg.rho_max); no_imp_ineq = 0
449
+
450
+ # -------- finalize for next iteration --------
451
+ prev_eq_inf = max(eq_inf_smooth, eps)
452
+ prev_ineq_inf = max(ineq_inf_smooth, eps)
453
+
454
+ # optional decoding for lifted blocks
455
+ decoded: Dict[int, Union[int, float]] = {}
456
+ for blk in registry.blocks:
457
+ sl = np.array(blk.idx, int)
458
+ if len(sl) == 0:
459
+ continue
460
+ s = x[sl]
461
+ j = int(np.argmax(s))
462
+ decoded[sl[0]] = float(blk.levels[j])
463
+
464
+ return {"x": x, "decoded": decoded, "hist": hist}