diffapqp 0.1.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.
- diffapqp/__init__.py +11 -0
- diffapqp-0.1.0.dist-info/METADATA +457 -0
- diffapqp-0.1.0.dist-info/RECORD +14 -0
- diffapqp-0.1.0.dist-info/WHEEL +5 -0
- diffapqp-0.1.0.dist-info/licenses/LICENSE +201 -0
- diffapqp-0.1.0.dist-info/top_level.txt +2 -0
- lao/__init__.py +14 -0
- lao/__version__.py +1 -0
- lao/cvxpy_compat.py +69 -0
- lao/diffapqp/__init__.py +2 -0
- lao/diffapqp/layer.py +702 -0
- lao/diffapqp/solve_result.py +55 -0
- lao/diffapqp/value_layer.py +799 -0
- lao/utils.py +459 -0
lao/utils.py
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Helper functions
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import cvxpy as cp
|
|
6
|
+
import numpy as np
|
|
7
|
+
import scipy.sparse as ssparse
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _to_dense_array(matrix):
|
|
11
|
+
return matrix.toarray() if ssparse.issparse(matrix) else np.asarray(matrix)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _to_dense_vector(matrix):
|
|
15
|
+
return np.asarray(_to_dense_array(matrix)).reshape(-1)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _matrix_column(matrix, col_idx):
|
|
19
|
+
if ssparse.issparse(matrix):
|
|
20
|
+
return matrix[:, [col_idx]]
|
|
21
|
+
return matrix[:, col_idx]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _has_nonzero_entries(matrix):
|
|
25
|
+
if ssparse.issparse(matrix):
|
|
26
|
+
return bool(np.count_nonzero(matrix.data))
|
|
27
|
+
return bool(np.count_nonzero(np.asarray(matrix)))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _expr_has_parameters(expr):
|
|
31
|
+
return len(expr.parameters()) > 0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _iter_expression_tree(expr):
|
|
35
|
+
# Walk CVXPY expression nodes depth-first through their child args.
|
|
36
|
+
# including objective and constraints
|
|
37
|
+
yield expr
|
|
38
|
+
for child in getattr(expr, "args", []):
|
|
39
|
+
yield from _iter_expression_tree(child)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def validate_apqp_problem(
|
|
43
|
+
prob,
|
|
44
|
+
*,
|
|
45
|
+
canonicalize_solver="OSQP",
|
|
46
|
+
require_dpp=True,
|
|
47
|
+
require_continuous=True,
|
|
48
|
+
):
|
|
49
|
+
"""Validate that a CVXPY problem matches the APQP/APLP form expected by DiffAPQP.
|
|
50
|
+
|
|
51
|
+
Accepted form:
|
|
52
|
+
|
|
53
|
+
min 1/2 x^T P x + q(theta)^T x
|
|
54
|
+
s.t. A x = b(theta)
|
|
55
|
+
G x <= h(theta)
|
|
56
|
+
|
|
57
|
+
where parameters may appear affinely in the linear objective and constraint
|
|
58
|
+
offsets. Equivalent syntactic placements such as ``x + p <= c`` are
|
|
59
|
+
accepted if CVXPY canonicalization leaves the variable coefficient matrices
|
|
60
|
+
parameter-free. Parameterized P, A, or G are rejected.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
# minimum test
|
|
64
|
+
if prob is None:
|
|
65
|
+
raise ValueError("A CVXPY problem instance is required for validation.")
|
|
66
|
+
|
|
67
|
+
if not isinstance(prob.objective, cp.Minimize):
|
|
68
|
+
raise ValueError("DiffAPQP only supports minimization problems.")
|
|
69
|
+
|
|
70
|
+
if not prob.is_dcp():
|
|
71
|
+
raise ValueError("DiffAPQP requires a DCP-compliant CVXPY problem.")
|
|
72
|
+
|
|
73
|
+
if not prob.is_qp():
|
|
74
|
+
raise ValueError("DiffAPQP only supports QP/LP problems in CVXPY's QP class.")
|
|
75
|
+
|
|
76
|
+
if prob.parameters() and require_dpp and not prob.is_dpp():
|
|
77
|
+
raise ValueError(
|
|
78
|
+
"DiffAPQP requires a DPP-compliant parametric problem. "
|
|
79
|
+
"Parameters must enter affinely in the supported APQP/APLP form."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Parameters may not appear inside the quadratic matrix P(theta).
|
|
83
|
+
for node in _iter_expression_tree(prob.objective.expr):
|
|
84
|
+
if type(node).__name__ == "QuadForm" and _expr_has_parameters(node):
|
|
85
|
+
raise ValueError(
|
|
86
|
+
"DiffAPQP does not support parameterized quadratic matrices. "
|
|
87
|
+
"Parameters may appear only in the linear objective term and "
|
|
88
|
+
"constraint right-hand sides."
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Constraints must be affine in variables/parameters. We do not require the
|
|
92
|
+
# original CVXPY left-hand side to be parameter-free, because expressions
|
|
93
|
+
# like x + p <= c are equivalent to x <= c - p and still have constant
|
|
94
|
+
# variable coefficients after canonicalization.
|
|
95
|
+
for idx, constr in enumerate(prob.constraints):
|
|
96
|
+
if not constr.expr.is_affine():
|
|
97
|
+
raise ValueError(
|
|
98
|
+
f"Constraint {idx} is not affine. DiffAPQP supports only "
|
|
99
|
+
"affine equality and inequality constraints."
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Currently we only support continuous problems: all variables must be continuous.
|
|
103
|
+
if require_continuous:
|
|
104
|
+
has_discrete_vars = any(
|
|
105
|
+
bool(getattr(var, "boolean_idx", [])) or bool(getattr(var, "integer_idx", []))
|
|
106
|
+
for var in prob.variables()
|
|
107
|
+
)
|
|
108
|
+
if has_discrete_vars:
|
|
109
|
+
raise ValueError(
|
|
110
|
+
"DiffAPQP layers currently support only continuous problems. "
|
|
111
|
+
"Relax integer/boolean variables before using the differentiable layers."
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
param_qp_prog, _, zero_dim, int_dim, bool_dim = return_compiler(
|
|
115
|
+
prob, canonicalize_solver=canonicalize_solver
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
if _has_nonzero_entries(param_qp_prog.P[:, :-1]):
|
|
119
|
+
raise ValueError(
|
|
120
|
+
"DiffAPQP does not support parameterized quadratic matrices. "
|
|
121
|
+
"Parameters may appear only in the linear objective term and "
|
|
122
|
+
"constraint offsets."
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
no_cons = param_qp_prog.constr_size
|
|
126
|
+
if no_cons > 0 and _has_nonzero_entries(param_qp_prog.A[:-no_cons, :-1]):
|
|
127
|
+
raise ValueError(
|
|
128
|
+
"DiffAPQP does not support parameterized constraint matrices. "
|
|
129
|
+
"Parameters may shift constraint offsets, but A and G must remain constant."
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if require_continuous:
|
|
133
|
+
if len(int_dim) > 0 or len(bool_dim) > 0:
|
|
134
|
+
raise ValueError(
|
|
135
|
+
"DiffAPQP layers currently support only continuous problems. "
|
|
136
|
+
"Relax integer/boolean variables before using the differentiable layers."
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def is_apqp_problem(prob, **kwargs):
|
|
141
|
+
"""Return True iff ``validate_apqp_problem`` succeeds."""
|
|
142
|
+
try:
|
|
143
|
+
validate_apqp_problem(prob, **kwargs)
|
|
144
|
+
except Exception:
|
|
145
|
+
return False
|
|
146
|
+
return True
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _flattened_size(shape):
|
|
150
|
+
"""Return flattened size for scalar/vector/matrix CVXPY objects. This is also the number of elements"""
|
|
151
|
+
if len(shape) == 0:
|
|
152
|
+
return 1
|
|
153
|
+
return int(np.prod(shape))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def flatten_fortran_numpy_sample(value):
|
|
157
|
+
"""Flatten a single CVXPY-shaped value using default CVXPY/Fortran order."""
|
|
158
|
+
arr = np.asarray(value)
|
|
159
|
+
if arr.ndim == 0:
|
|
160
|
+
# scalar
|
|
161
|
+
return np.array([arr.item()])
|
|
162
|
+
if arr.ndim == 1:
|
|
163
|
+
# vector
|
|
164
|
+
return arr
|
|
165
|
+
# matrix
|
|
166
|
+
return arr.reshape(-1, order="F")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def unflatten_fortran_torch_batch(x_flat, shape):
|
|
170
|
+
"""Recover batched flat CVXPY data into its original shape."""
|
|
171
|
+
# x_flat.shape == (batch_size, flattened_size)
|
|
172
|
+
# shape is the original shape of the variable
|
|
173
|
+
if len(shape) == 0:
|
|
174
|
+
return x_flat[:, 0]
|
|
175
|
+
if len(shape) == 1:
|
|
176
|
+
return x_flat
|
|
177
|
+
|
|
178
|
+
view_shape = (x_flat.shape[0], *reversed(shape))
|
|
179
|
+
permute_dims = (0, *range(len(shape), 0, -1))
|
|
180
|
+
return x_flat.reshape(view_shape).permute(permute_dims)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def flatten_fortran_torch_batch(x, shape):
|
|
184
|
+
"""Flatten batched original-shaped data using CVXPY/Fortran order."""
|
|
185
|
+
if len(shape) == 0:
|
|
186
|
+
return x.reshape(x.shape[0], 1)
|
|
187
|
+
if len(shape) == 1:
|
|
188
|
+
return x.reshape(x.shape[0], -1)
|
|
189
|
+
|
|
190
|
+
permute_dims = (0, *range(x.ndim - 1, 0, -1))
|
|
191
|
+
return x.permute(permute_dims).reshape(x.shape[0], -1)
|
|
192
|
+
|
|
193
|
+
def return_compiler(prob, canonicalize_solver="OSQP"):
|
|
194
|
+
"""
|
|
195
|
+
return the compiler of the problem given by cvxpy
|
|
196
|
+
args:
|
|
197
|
+
- prob: the cvxpy problem
|
|
198
|
+
- canonicalize_solver: cvxpy solver name used for problem stuffing
|
|
199
|
+
default is open-source solver OSQP
|
|
200
|
+
return:
|
|
201
|
+
- param_qp_prog: the qp problem in standard form
|
|
202
|
+
- params_idx: {param_id: param_name}, link the id to the parameter name
|
|
203
|
+
- zero_dim: the number of equality constraint in the standard form
|
|
204
|
+
(always be the first rows in the matrix)
|
|
205
|
+
- int_vars_idx: the index of integer variables
|
|
206
|
+
- bool_vars_idx: the index of boolean variables
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
# canonicalize the problem
|
|
210
|
+
solver_name = canonicalize_solver.upper()
|
|
211
|
+
if not hasattr(cp, solver_name):
|
|
212
|
+
raise ValueError(f"Unknown CVXPY solver for canonicalization: {canonicalize_solver}")
|
|
213
|
+
solver = getattr(cp, solver_name)
|
|
214
|
+
solver_opts = {"use_quad_obj": True} if solver_name == "GUROBI" else {}
|
|
215
|
+
data, _, _ = prob.get_problem_data(
|
|
216
|
+
solver=solver, solver_opts=solver_opts)
|
|
217
|
+
|
|
218
|
+
assert prob.is_qp(), 'only support QP (and LP) for now'
|
|
219
|
+
assert data['dims'].exp == 0, 'does not support exponential cone'
|
|
220
|
+
assert len(data['dims'].psd) == 0, 'does not support positive semidefinite cone'
|
|
221
|
+
# assert len(data['dims'].soc) == 0, 'does not support second-order cone'
|
|
222
|
+
|
|
223
|
+
# parametric QP problem
|
|
224
|
+
param_qp_prog = data[cp.settings.PARAM_PROB]
|
|
225
|
+
|
|
226
|
+
# ! the order of parameter idx is changed internally in cvxpy so we link the parameter id to its name
|
|
227
|
+
params_idx = {p.id: p.name() for p in prob.parameters()}
|
|
228
|
+
|
|
229
|
+
return param_qp_prog, params_idx, data['dims'].zero, data['int_vars_idx'], data['bool_vars_idx']
|
|
230
|
+
|
|
231
|
+
def return_standard_form(prob, sparse=False, canonicalize_solver="OSQP"):
|
|
232
|
+
"""
|
|
233
|
+
This function will identify all the
|
|
234
|
+
parameters (data) from the param_qp_prog and return the standard form.
|
|
235
|
+
|
|
236
|
+
The standard form we consider is:
|
|
237
|
+
min 1/2 x^T P x + (q + sum Q_i z_i)^T x + c + sum d_i^T z_i
|
|
238
|
+
s.t. A x = b + sum B_i z_i
|
|
239
|
+
G x <= h + sum H_i z_i
|
|
240
|
+
in which x is the decision variable, z_i is the i-th parameter.
|
|
241
|
+
args:
|
|
242
|
+
- prob: the cvxpy problem
|
|
243
|
+
- sparse: whether return sparse matrices
|
|
244
|
+
- canonicalize_solver: cvxpy solver name used for problem stuffing
|
|
245
|
+
"""
|
|
246
|
+
if prob is not None:
|
|
247
|
+
validate_apqp_problem(prob, canonicalize_solver=canonicalize_solver)
|
|
248
|
+
|
|
249
|
+
# canonicalize the problem
|
|
250
|
+
param_qp_prog, param_id_to_name, zero_dim, int_dim, bool_dim = return_compiler(
|
|
251
|
+
prob, canonicalize_solver=canonicalize_solver
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# Get the total number of constraints and variables
|
|
255
|
+
no_cons = param_qp_prog.constr_size
|
|
256
|
+
no_var = param_qp_prog.reduced_A.var_len
|
|
257
|
+
|
|
258
|
+
# Extract the constant quadratic cost matrix P without densifying the full
|
|
259
|
+
# parameter-to-P map, which has no_var**2 rows and can be enormous.
|
|
260
|
+
P = _to_dense_vector(_matrix_column(param_qp_prog.P, -1)).reshape(no_var, no_var)
|
|
261
|
+
|
|
262
|
+
# Extract the linear cost vector q
|
|
263
|
+
q_raw = _to_dense_array(param_qp_prog.q)
|
|
264
|
+
q = q_raw[:-1,-1] # q vector
|
|
265
|
+
Q_tilde = q_raw[:-1,:-1] # parameter matrices
|
|
266
|
+
c = float(q_raw[-1, -1]) # scalar objective offset
|
|
267
|
+
d_tilde = q_raw[-1, :-1] # parameter-to-offset vectors
|
|
268
|
+
|
|
269
|
+
# Extract and reshape the constraint matrix A_tilde
|
|
270
|
+
# This contains both equality (Ax=b) and inequality constraints (Gx<=h)
|
|
271
|
+
if no_cons > 0:
|
|
272
|
+
A_const_col = _to_dense_vector(_matrix_column(param_qp_prog.A, -1))
|
|
273
|
+
# A_tilde = A_raw[:int(no_cons * no_var), -1]
|
|
274
|
+
A_tilde = A_const_col[:-no_cons].reshape(no_var, no_cons).T
|
|
275
|
+
# b_tilde = A_raw[int(no_cons * no_var):, -1]
|
|
276
|
+
b_tilde = A_const_col[-no_cons:]
|
|
277
|
+
else:
|
|
278
|
+
# Keep explicit empty objects for unconstrained problems.
|
|
279
|
+
A_tilde = np.zeros((0, no_var))
|
|
280
|
+
b_tilde = np.zeros((0,))
|
|
281
|
+
|
|
282
|
+
# Split A_tilde into:
|
|
283
|
+
# A: equality constraints (first zero_dim rows)
|
|
284
|
+
# G: inequality constraints (remaining rows, with negative sign)
|
|
285
|
+
A = A_tilde[:zero_dim,:]
|
|
286
|
+
G = -A_tilde[zero_dim:,:]
|
|
287
|
+
|
|
288
|
+
# Split b_tilde into:
|
|
289
|
+
# b: equality constraints right-hand side (with negative sign)
|
|
290
|
+
# h: inequality constraints right-hand side
|
|
291
|
+
b = -b_tilde[:zero_dim]
|
|
292
|
+
h = b_tilde[zero_dim:]
|
|
293
|
+
|
|
294
|
+
# Extract the parameter coefficient matrix B_tilde
|
|
295
|
+
# B_tilde = A_raw[int(no_cons * no_var):, :-1]
|
|
296
|
+
if no_cons > 0:
|
|
297
|
+
B_tilde = _to_dense_array(param_qp_prog.A[-no_cons:, :-1])
|
|
298
|
+
else:
|
|
299
|
+
B_tilde = np.zeros((0, param_qp_prog.A.shape[1] - 1))
|
|
300
|
+
|
|
301
|
+
# Initialize dictionaries for parameter matrices
|
|
302
|
+
B = {} # For equality constraints (B_i)
|
|
303
|
+
H = {} # For inequality constraints (H_i)
|
|
304
|
+
Q = {} # For linear cost parameters (Q_i)
|
|
305
|
+
d = {} # For scalar objective-offset parameters (d_i)
|
|
306
|
+
|
|
307
|
+
# Get mappings for parameters
|
|
308
|
+
param_id_to_col = param_qp_prog.param_id_to_col # Maps parameter ID to starting column
|
|
309
|
+
param_id_to_size = param_qp_prog.param_id_to_size # Maps parameter ID to its size
|
|
310
|
+
var_name_to_size = {
|
|
311
|
+
v.name(): _flattened_size(v.shape) for v in param_qp_prog.variables
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
# For each parameter:
|
|
315
|
+
for key, start_idx in param_id_to_col.items():
|
|
316
|
+
if key == -1: # Skip if key is -1 (special value)
|
|
317
|
+
continue
|
|
318
|
+
|
|
319
|
+
size = param_id_to_size[key] # Get parameter size
|
|
320
|
+
name = param_id_to_name[key] # Get parameter name
|
|
321
|
+
|
|
322
|
+
# Extract parameter matrices:
|
|
323
|
+
# B[name]: how parameter affects equality constraints
|
|
324
|
+
# H[name]: how parameter affects inequality constraints
|
|
325
|
+
B[name] = -B_tilde[:zero_dim, start_idx:start_idx+size]
|
|
326
|
+
H[name] = B_tilde[zero_dim:, start_idx:start_idx+size]
|
|
327
|
+
Q[name] = Q_tilde[:, start_idx:start_idx+size]
|
|
328
|
+
d[name] = d_tilde[start_idx:start_idx+size]
|
|
329
|
+
|
|
330
|
+
if sparse:
|
|
331
|
+
P = ssparse.csc_matrix(P)
|
|
332
|
+
A = ssparse.csc_matrix(A)
|
|
333
|
+
G = ssparse.csc_matrix(G)
|
|
334
|
+
for key in B.keys():
|
|
335
|
+
B[key] = ssparse.csc_matrix(B[key])
|
|
336
|
+
for key in H.keys():
|
|
337
|
+
H[key] = ssparse.csc_matrix(H[key])
|
|
338
|
+
|
|
339
|
+
return P, q, Q, c, d, A, G, b, h, B, H, var_name_to_size, int_dim, bool_dim # Q, d, B, H are dictionaries
|
|
340
|
+
|
|
341
|
+
def return_standard_form_in_cvxpy(prob, sparse=False, canonicalize_solver="OSQP"):
|
|
342
|
+
"""
|
|
343
|
+
Return the standard form of the problem formulated as cvxpy problem
|
|
344
|
+
args:
|
|
345
|
+
- prob: the cvxpy problem, the parameters must be assigned a name
|
|
346
|
+
- sparse: whether to return the sparse matrix, default is False
|
|
347
|
+
- canonicalize_solver: cvxpy solver name used for problem stuffing
|
|
348
|
+
return:
|
|
349
|
+
- prob: the cvxpy problem in standard form
|
|
350
|
+
- prob_data: the data of the problem, including P, q, c, A, G, b, h, Q, d, B, H
|
|
351
|
+
Standard form:
|
|
352
|
+
min 1/2 x^T P x + q^T x + c
|
|
353
|
+
s.t. A x = b + sum B_i z_i
|
|
354
|
+
G x <= h + sum H_i z_i
|
|
355
|
+
in which x is the decision variable, z_i is the i-th parameter
|
|
356
|
+
NOTE: the formulation accepts multiple parameters for both inequality and equality constraints
|
|
357
|
+
NOTE: you still need to assign the parameters by param.value = your_value to the problem after this conversion
|
|
358
|
+
NOTE: Only support continuous variables, So for discrete QP, the discrete variables must be relaxed first.
|
|
359
|
+
"""
|
|
360
|
+
|
|
361
|
+
P, q, Q, c, d, A, G, b, h, B, H, var_name_to_size, int_dim, bool_dim = return_standard_form(
|
|
362
|
+
prob, sparse=sparse, canonicalize_solver=canonicalize_solver
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
x = cp.Variable(P.shape[1])
|
|
366
|
+
# Build parameters from the union of objective/constraint parameter sets.
|
|
367
|
+
# Some parameters may appear only in Q (objective), or only in B/H (constraints).
|
|
368
|
+
param_keys = list(dict.fromkeys(list(Q.keys()) + list(d.keys()) + list(B.keys()) + list(H.keys())))
|
|
369
|
+
parameters = {}
|
|
370
|
+
for key in param_keys:
|
|
371
|
+
if key in Q:
|
|
372
|
+
size = Q[key].shape[1]
|
|
373
|
+
elif key in d:
|
|
374
|
+
size = d[key].shape[0]
|
|
375
|
+
elif key in B:
|
|
376
|
+
size = B[key].shape[1]
|
|
377
|
+
else:
|
|
378
|
+
size = H[key].shape[1]
|
|
379
|
+
parameters[key] = cp.Parameter(size, name=key)
|
|
380
|
+
|
|
381
|
+
# formulate the cvxpy problem
|
|
382
|
+
constraints = []
|
|
383
|
+
|
|
384
|
+
q_ = cp.Constant(np.zeros(P.shape[0]))
|
|
385
|
+
c_ = cp.Constant(c)
|
|
386
|
+
b_ = cp.Constant(np.zeros(A.shape[0])) if A.shape[0] > 0 else None
|
|
387
|
+
h_ = cp.Constant(np.zeros(G.shape[0])) if G.shape[0] > 0 else None
|
|
388
|
+
|
|
389
|
+
for key in parameters.keys():
|
|
390
|
+
if key in B and b_ is not None:
|
|
391
|
+
b_ += B[key] @ parameters[key]
|
|
392
|
+
if key in H and h_ is not None:
|
|
393
|
+
h_ += H[key] @ parameters[key]
|
|
394
|
+
if key in Q:
|
|
395
|
+
q_ += Q[key] @ parameters[key]
|
|
396
|
+
if key in d:
|
|
397
|
+
c_ += d[key] @ parameters[key]
|
|
398
|
+
|
|
399
|
+
if A.shape[0] > 0:
|
|
400
|
+
constraints.append(A @ x == b_ + b)
|
|
401
|
+
if G.shape[0] > 0:
|
|
402
|
+
constraints.append(G @ x <= h_ + h)
|
|
403
|
+
|
|
404
|
+
objective = cp.Minimize(0.5 * cp.quad_form(x, cp.psd_wrap(P)) + (q_ + q) @ x + c_)
|
|
405
|
+
prob = cp.Problem(objective, constraints)
|
|
406
|
+
|
|
407
|
+
return prob, var_name_to_size, {"P": P, "Q": Q, "q": q, "c": c, "d": d, "A": A, "G": G, "b": b, "h": h, "B": B, "H": H}
|
|
408
|
+
|
|
409
|
+
def get_default_solver_args(solver, eps = None):
|
|
410
|
+
|
|
411
|
+
if solver.upper() == "OSQP":
|
|
412
|
+
# see https://osqp.org/docs/interfaces/solver_settings.html
|
|
413
|
+
solver_args = {
|
|
414
|
+
"eps_abs": 1e-5 if eps is None else eps, # in cvxpy, the default is 1e-5
|
|
415
|
+
"eps_rel": 1e-5 if eps is None else eps, # in cvxpy, the default is 1e-5
|
|
416
|
+
"max_iter": 100000,
|
|
417
|
+
}
|
|
418
|
+
elif solver.upper() == "GUROBI":
|
|
419
|
+
solver_args = {
|
|
420
|
+
"Threads": 1, # this will limit the number of threads to 1, each problem is solved in a single cpu core
|
|
421
|
+
"OptimalityTol": 1e-8 if eps is None else eps, # set as default # ! for simplex method
|
|
422
|
+
"FeasibilityTol": 1e-8 if eps is None else eps, # set as default
|
|
423
|
+
"BarConvTol": 1e-8 if eps is None else eps, # ! this is for the barrier method
|
|
424
|
+
}
|
|
425
|
+
elif solver.upper() == "SCS":
|
|
426
|
+
solver_args = {
|
|
427
|
+
"eps_abs": 1e-6 if eps is None else eps, # default 1e-4
|
|
428
|
+
"eps_rel": 1e-6 if eps is None else eps, # default 1e-4
|
|
429
|
+
"max_iters": 10000,
|
|
430
|
+
}
|
|
431
|
+
elif solver.upper() == "ECOS":
|
|
432
|
+
solver_args = {
|
|
433
|
+
"abstol": 1e-8 if eps is None else eps,
|
|
434
|
+
"reltol": 1e-8 if eps is None else eps,
|
|
435
|
+
"max_iters": 200,
|
|
436
|
+
}
|
|
437
|
+
elif solver.upper() == "CLARABEL":
|
|
438
|
+
solver_args = {
|
|
439
|
+
"tol_gap_abs": 1e-8 if eps is None else eps,
|
|
440
|
+
"tol_gap_rel": 1e-8 if eps is None else eps,
|
|
441
|
+
"max_iter": 1000
|
|
442
|
+
}
|
|
443
|
+
elif solver.upper() == "QPALM":
|
|
444
|
+
solver_args = {
|
|
445
|
+
"eps_abs": 1e-6 if eps is None else eps,
|
|
446
|
+
"eps_rel": 1e-6 if eps is None else eps,
|
|
447
|
+
"max_iter": 10000,
|
|
448
|
+
}
|
|
449
|
+
elif solver.upper() == "PROXQP":
|
|
450
|
+
solver_args = {
|
|
451
|
+
"eps_abs": 1e-6 if eps is None else eps,
|
|
452
|
+
"eps_rel": 1e-6 if eps is None else eps,
|
|
453
|
+
"max_iter": 10000,
|
|
454
|
+
"backend": "sparse" # solve the memory issue
|
|
455
|
+
}
|
|
456
|
+
else:
|
|
457
|
+
solver_args = {}
|
|
458
|
+
|
|
459
|
+
return solver_args
|