Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions cvxpy/reductions/solvers/defines.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@
from cvxpy.reductions.solvers.conic_solvers.scs_conif import SCS as SCS_con
from cvxpy.reductions.solvers.conic_solvers.sdpa_conif import SDPA as SDPA_con
from cvxpy.reductions.solvers.conic_solvers.xpress_conif import XPRESS as XPRESS_con

# NLP interfaces
from cvxpy.reductions.solvers.nlp_solvers.copt_nlpif import COPT as COPT_nlp
from cvxpy.reductions.solvers.nlp_solvers.ipopt_nlpif import IPOPT as IPOPT_nlp
from cvxpy.reductions.solvers.nlp_solvers.uno_nlpif import UNO as UNO_nlp

Expand Down Expand Up @@ -85,7 +88,7 @@
MPAX_qp(),
KNITRO_qp(),
]
solver_nlp_intf = [IPOPT_nlp(), UNO_nlp()]
solver_nlp_intf = [IPOPT_nlp(), UNO_nlp(), COPT_nlp()]

SOLVER_MAP_CONIC = {solver.name(): solver for solver in solver_conic_intf}
SOLVER_MAP_QP = {solver.name(): solver for solver in solver_qp_intf}
Expand Down Expand Up @@ -138,7 +141,7 @@
s.MPAX,
s.KNITRO,
]
NLP_SOLVERS = [s.IPOPT, s.UNO]
NLP_SOLVERS = [s.IPOPT, s.UNO, s.COPT]
DISREGARD_CLARABEL_SDP_SUPPORT_FOR_DEFAULT_RESOLUTION = True
MI_SOLVERS = [
s.GLPK_MI,
Expand Down
189 changes: 175 additions & 14 deletions cvxpy/reductions/solvers/nlp_solvers/copt_nlpif.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,19 @@ class COPT(NLPsolver):
"""
NLP interface for the COPT solver.
"""
# Map between COPT status and CVXPY status
STATUS_MAP = {
# Success cases
0: s.OPTIMAL, # Solve_Succeeded
1: s.OPTIMAL_INACCURATE, # Solved_To_Acceptable_Level
6: s.OPTIMAL, # Feasible_Point_Found

# Infeasibility/Unboundedness
2: s.INFEASIBLE, # Infeasible_Problem_Detected
4: s.UNBOUNDED, # Diverging_Iterates
}
1: s.OPTIMAL, # optimal
2: s.INFEASIBLE, # infeasible
3: s.UNBOUNDED, # unbounded
4: s.INF_OR_UNB, # infeasible or unbounded
5: s.SOLVER_ERROR, # numerical
6: s.USER_LIMIT, # node limit
7: s.OPTIMAL_INACCURATE, # imprecise
8: s.USER_LIMIT, # time out
9: s.SOLVER_ERROR, # unfinished
10: s.USER_LIMIT # interrupted
}

def name(self):
"""
Expand All @@ -47,16 +50,18 @@ def import_solver(self):
"""
Imports the solver.
"""
import cyipopt # noqa F401
import coptpy # noqa F401

def invert(self, solution, inverse_data):
"""
Returns the solution to the original problem given the inverse_data.
"""
attr = {}
attr = {
s.NUM_ITERS: solution.get('num_iters'),
s.SOLVE_TIME: solution.get('solve_time_real'),
}

status = self.STATUS_MAP[solution['status']]
attr[s.NUM_ITERS] = solution['iterations']

if status in s.SOLUTION_PRESENT:
primal_val = solution['obj_val']
opt_val = primal_val + inverse_data.offset
Expand Down Expand Up @@ -106,7 +111,163 @@ def solve_via_data(self, data, warm_start: bool, verbose: bool, solver_opts, sol
tuple
(status, optimal value, primal, equality dual, inequality dual)
"""
raise NotImplementedError("COPT NLP interface is not yet implemented.")
import coptpy as copt

class COPTNlpCallbackCVXPY(copt.NlpCallbackBase):
def __init__(self, oracles, m):
super().__init__()
self._oracles = oracles
self._m = m

def EvalObj(self, xdata, outdata):
x = copt.NdArray(xdata)
outval = copt.NdArray(outdata)

x_np = x.tonumpy()
outval_np = self._oracles.objective(x_np)

outval[:] = outval_np
return 0

def EvalGrad(self, xdata, outdata):
x = copt.NdArray(xdata)
outval = copt.NdArray(outdata)

x_np = x.tonumpy()
outval_np = self._oracles.gradient(x_np)

outval[:] = np.asarray(outval_np).flatten()
return 0

def EvalCon(self, xdata, outdata):
if self._m > 0:
x = copt.NdArray(xdata)
outval = copt.NdArray(outdata)

x_np = x.tonumpy()
outval_np = self._oracles.constraints(x_np)

outval[:] = np.asarray(outval_np).flatten()
return 0

def EvalJac(self, xdata, outdata):
if self._m > 0:
x = copt.NdArray(xdata)
outval = copt.NdArray(outdata)

x_np = x.tonumpy()
outval_np = self._oracles.jacobian(x_np)

outval[:] = np.asarray(outval_np).flatten()
return 0

def EvalHess(self, xdata, sigma, lambdata, outdata):
x = copt.NdArray(xdata)
lagrange = copt.NdArray(lambdata)
outval = copt.NdArray(outdata)

x_np = x.tonumpy()
lagrange_np = lagrange.tonumpy()
outval_np = self._oracles.hessian(x_np, lagrange_np, sigma)

outval[:] = np.asarray(outval_np).flatten()
return 0

# Create COPT environment and model
envconfig = copt.EnvrConfig()
if not verbose:
envconfig.set('nobanner', '1')

env = copt.Envr(envconfig)
model = env.createModel()

# Pass through verbosity
model.setParam(copt.COPT.Param.Logging, verbose)

# Get oracles for function evaluation
oracles = data['oracles']

# Get the NLP problem data
x0 = data['x0']
lb, ub = data['lb'].copy(), data['ub'].copy()
cl, cu = data['cl'].copy(), data['cu'].copy()

lb[lb == -np.inf] = -copt.COPT.INFINITY
ub[ub == +np.inf] = +copt.COPT.INFINITY
cl[cl == -np.inf] = -copt.COPT.INFINITY
cu[cu == +np.inf] = +copt.COPT.INFINITY

n = len(lb)
m = len(cl)

cbtype = copt.COPT.EVALTYPE_OBJVAL | copt.COPT.EVALTYPE_CONSTRVAL | \
copt.COPT.EVALTYPE_GRADIENT | copt.COPT.EVALTYPE_JACOBIAN | \
copt.COPT.EVALTYPE_HESSIAN
cbfunc = COPTNlpCallbackCVXPY(oracles, m)

if m > 0:
jac_rows, jac_cols = oracles.jacobianstructure()
nnz_jac = len(jac_rows)
else:
jac_rows = None
jac_cols = None
nnz_jac = 0

if n > 0:
hess_rows, hess_cols = oracles.hessianstructure()
nnz_hess = len(hess_rows)
else:
hess_rows = None
hess_cols = None
nnz_hess = 0

# Load NLP problem data
model.loadNlData(n, # Number of variables
m, # Number of constraints
copt.COPT.MINIMIZE, # Objective sense
copt.COPT.DENSETYPE_ROWMAJOR, None, # Dense objective gradient
nnz_jac, jac_rows, jac_cols, # Sparse jacobian
nnz_hess, hess_rows, hess_cols, # Sparse hessian
lb, ub, # Variable bounds
cl, cu, # Constraint bounds
x0, # Starting point
cbtype, cbfunc # Callback function
)

# Set parameters
for key, value in solver_opts.items():
model.setParam(key, value)

# Solve problem
model.solve()

# Get solution
nlp_status = model.status
nlp_hassol = model.haslpsol

if nlp_hassol:
objval = model.objval
x_sol = model.getValues()
lambda_sol = model.getDuals()
else:
objval = +np.inf
x_sol = [0.0] * n
lambda_sol = [0.0] * m

num_iters = model.barrieriter
solve_time_real = model.solvingtime

# Return results in dictionary format expected by invert()
solution = {
'status': nlp_status,
'obj_val': objval,
'x': np.array(x_sol),
'lambda': np.array(lambda_sol),
'num_iters': num_iters,
'solve_time_real': solve_time_real
}

return solution

def cite(self, data):
"""Returns bibtex citation for the solver.
Expand Down
19 changes: 16 additions & 3 deletions cvxpy/tests/nlp_tests/test_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,21 @@ def test_knitro_solver_stats(self):
@pytest.mark.skipif('COPT' not in INSTALLED_SOLVERS, reason='COPT is not installed.')
class TestCOPTInterface:

def test_copt_call(self):
def test_copt_basic_solve(self):
"""Test that COPT can solve a basic NLP problem."""
x = cp.Variable()
prob = cp.Problem(cp.Minimize((x - 2) ** 2), [x >= 1])
with pytest.raises(NotImplementedError):
prob.solve(solver=cp.COPT, nlp=True)
prob.solve(solver=cp.COPT, nlp=True)
assert prob.status == cp.OPTIMAL
assert np.isclose(x.value, 2.0, atol=1e-5)

def test_copt_maxit(self):
"""Test maximum iterations option."""
x = cp.Variable(2)
x.value = np.array([1.0, 1.0])
prob = cp.Problem(cp.Minimize(x[0]**2 + x[1]**2), [x[0] + x[1] >= 1])

# Use a reasonable maxit value that allows convergence
prob.solve(solver=cp.COPT, nlp=True, NLPIterLimit=100)
assert prob.status == cp.OPTIMAL
assert np.allclose(x.value, [0.5, 0.5], atol=1e-4)
4 changes: 3 additions & 1 deletion cvxpy/tests/nlp_tests/test_nlp_solvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
not is_knitro_available(), reason='KNITRO is not installed or license not available.')),
pytest.param('UNO', marks=pytest.mark.skipif(
'UNO' not in INSTALLED_SOLVERS, reason='UNO is not installed.')),
pytest.param('COPT', marks=pytest.mark.skipif(
'COPT' not in INSTALLED_SOLVERS, reason='COPT is not installed.')),
]


Expand Down Expand Up @@ -246,7 +248,7 @@ def test_circle_packing_formulation_two(self, solver):
min_dist_sq = (radius[i] + radius[j]) ** 2
residuals.append(dist_sq - min_dist_sq)

assert(np.all(np.array(residuals) <= 1e-8))
assert(np.all(np.array(residuals) <= 1e-6))

# Ipopt finds these centers, but Knitro rotates them (but finds the same
# objective value)
Expand Down
Loading
Loading