cvxgrp / cvxpygen

Code generation with CVXPY

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

CPG Solver Produces 'primal infeasible' Solution for Conventionally Solvable Problem

htrocks opened this issue · comments

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.

using cvxpy 1.4.1 solved problem.