CPG Solver Produces 'primal infeasible' Solution for Conventionally Solvable Problem
htrocks opened this issue · comments
htrocks commented
Thanks for developing this great project.
When using the CPG solver from the cvxpygen project on a valid optimization problem, the solver reports a 'primal infeasible' solution, while the conventional solver produces an optimal solution.
Minimal example reproducing the problem
import cvxpy as cp
import numpy as np
import sys
import os
import pickle
from cvxpygen import cpg
# define var & param
n = 7
var_0 = cp.Variable(n, name="var_0")
var_1 = cp.Variable(n, name="var_1")
param_0 = cp.Parameter(n, name="param_0")
param_1 = cp.Parameter(n, name="param_1")
param_2 = cp.Parameter(n, nonneg=True, name="param_2")
param_3 = cp.Parameter(n, name="param_3")
param_4 = cp.Parameter(n, name="param_4")
param_5 = cp.Parameter((n, n), name="param_5")
# define objective
objective = cp.Maximize(param_0.T @ var_0 - param_2 @ cp.abs(var_1) - cp.sum_squares((3 * param_5) @ var_0))
# define constraints
constraints = []
constraints.append(var_0 == param_1 + var_1)
constraints.append(cp.abs(cp.sum(var_0)) <= 2)
constraints.append(var_1 <= param_3)
constraints.append(var_1 >= -param_4)
for i in range(n):
constraints.append(var_0[i] <= 1)
constraints.append(var_0[i] >= -1)
# define problem
problem = cp.Problem(objective, constraints)
# generate C source
assert os.path.isfile(f"{os.getcwd()}/__init__.py"), "Must run this in the root of a packege. e.g., $ mkdir -p /tmp/foo && touch /tmp/foo/__init__.py "
sys.path.insert(0, os.getcwd()) # manually add the path for safety
cpg.generate_code(problem, code_dir='my_test', solver='OSQP', wrapper=True)
from my_test.cpg_solver import cpg_solve
del problem # to avoid mistaken using
# load the problem
with open("my_test/problem.pickle", "rb") as f:
prob = pickle.load(f)
# assign param value
def make_array(shape):
return np.arange(0, np.prod(shape), dtype='<f8').reshape(shape)
prob.param_dict['param_0'].value = make_array([n])
prob.param_dict['param_1'].value = make_array([n])
prob.param_dict['param_2'].value = make_array([n])
prob.param_dict['param_3'].value = make_array([n])
prob.param_dict['param_4'].value = make_array([n])
prob.param_dict['param_5'].value = make_array([n, n])
# solve problem conventionally
solver_params={"solver": "OSQP", "enforce_dpp": True, "max_iter": 10000, "eps_abs": 1e-6, "eps_rel": 1e-6}
prob.solve(**solver_params)
print("Conventional:")
print(dict(status=prob.solution.status, opt_val=prob.solution.opt_val, num_iters=prob.solution.attr["num_iters"]))
# solve using CPG
solver_params.pop('solver')
solver_params.pop('enforce_dpp')
prob.register_solve('CPG', cpg_solve)
prob.solve(method='CPG', **solver_params)
print("\nCPG:")
print(dict(status=prob.solution.status, opt_val=prob.solution.opt_val, num_iters=prob.solution.attr["num_iters"]))
Output
...
Conventional:
{'status': 'optimal', 'opt_val': -90.9993696512673, 'num_iters': 7500}
CPG:
{'status': 'dual infeasible', 'opt_val': inf, 'num_iters': 125}
Version
cvxpygen 0.3.1
cvxpy 1.3.1
osqp 0.6.3
What I tried
I made a private build of osqp which prints the workspace inside osqp_solve
. Turned out the only differences are the signs of some fields in A
, l
, u
. For example, for A.x
:
...
Numerical Values (x):
0: 0.9195
1: 0.9426
2: 0.9599
3: 0.9733
4: 0.9839
5: 0.9927
6: 0.9999
7: 0.0665
- 8: -0.0665
- 9: 0.0665
- 10: -0.9953
- 11: 0.9953
+ 8: 0.0665
+ 9: -0.0665
+ 10: 0.9953
+ 11: -0.9953
12: 0.9336
13: 0.9527
14: 0.9669
15: 0.9779
16: 0.9868
17: 0.9939
18: 0.9999
19: 0.0657
- 20: -0.0657
- 21: 0.0657
- 22: -0.9952
- 23: 0.9952
+ 20: 0.0657
+ 21: -0.0657
+ 22: 0.9952
+ 23: -0.9952
...
It would be highly appreciated if anyone can have a look when getting a moment. Thanks.
htrocks commented
using cvxpy 1.4.1 solved problem.