Skip to content

Commit 0ef0dab

Browse files
committed
Add NONCONVEX_QUADRATIC_CONSTRAINT_SOLVERS constant and adjust tests
1 parent df45999 commit 0ef0dab

File tree

2 files changed

+30
-22
lines changed

2 files changed

+30
-22
lines changed

linopy/solvers.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,15 @@
6161
"copt",
6262
]
6363

64+
# Solvers that support nonconvex quadratic constraints (bilinear terms, >= constraints)
65+
# MOSEK and COPT only support convex quadratic constraints
66+
NONCONVEX_QUADRATIC_CONSTRAINT_SOLVERS = [
67+
"gurobi",
68+
"xpress",
69+
"cplex",
70+
"scip",
71+
]
72+
6473
# Solvers that don't need a solution file when keep_files=False
6574
NO_SOLUTION_FILE_SOLVERS = [
6675
"xpress",
@@ -160,6 +169,9 @@ class xpress_Namespaces: # type: ignore[no-redef]
160169
quadratic_constraint_solvers = [
161170
s for s in QUADRATIC_CONSTRAINT_SOLVERS if s in available_solvers
162171
]
172+
nonconvex_quadratic_constraint_solvers = [
173+
s for s in NONCONVEX_QUADRATIC_CONSTRAINT_SOLVERS if s in available_solvers
174+
]
163175
logger = logging.getLogger(__name__)
164176

165177

test/test_quadratic_constraint.py

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616
import linopy
1717
from linopy import Model
1818
from linopy.constraints import QuadraticConstraint
19-
from linopy.solvers import available_solvers, quadratic_constraint_solvers
19+
from linopy.solvers import (
20+
available_solvers,
21+
nonconvex_quadratic_constraint_solvers,
22+
quadratic_constraint_solvers,
23+
)
2024

2125
# Build parameter list: (solver, io_api) for QC-capable solvers
2226
qc_solver_params: list[tuple[str, str]] = []
@@ -26,6 +30,14 @@
2630
if solver in ["gurobi", "mosek"]:
2731
qc_solver_params.append((solver, "direct"))
2832

33+
# Build parameter list for nonconvex QC tests
34+
nonconvex_qc_solver_params: list[tuple[str, str]] = []
35+
for solver in nonconvex_quadratic_constraint_solvers:
36+
if solver in available_solvers:
37+
nonconvex_qc_solver_params.append((solver, "lp"))
38+
if solver == "gurobi":
39+
nonconvex_qc_solver_params.append((solver, "direct"))
40+
2941

3042
@pytest.fixture
3143
def m() -> Model:
@@ -1020,15 +1032,11 @@ def test_qc_multidim_solution(
10201032
assert np.allclose(y_vals, 4.472, atol=0.01)
10211033
assert np.isclose(obj_val, 3 * 11.18, atol=0.05) # 3x single solution
10221034

1023-
@pytest.mark.parametrize("solver,io_api", qc_solver_params)
1035+
@pytest.mark.parametrize("solver,io_api", nonconvex_qc_solver_params)
10241036
def test_qc_mixed_linear_quad(
10251037
self, qc_mixed_model: Model, solver: str, io_api: str
10261038
) -> None:
1027-
"""Test QC with both quadratic and linear terms."""
1028-
# COPT does not handle this nonconvex constraint (x-1)² <= 0
1029-
if solver == "copt":
1030-
pytest.skip("COPT does not support this nonconvex constraint form")
1031-
1039+
"""Test QC with both quadratic and linear terms (nonconvex)."""
10321040
status, condition = qc_mixed_model.solve(solver, io_api=io_api)
10331041
assert status == "ok"
10341042
assert condition == "optimal"
@@ -1037,15 +1045,11 @@ def test_qc_mixed_linear_quad(
10371045
x_val = float(qc_mixed_model.solution["x"].values)
10381046
assert np.isclose(x_val, 1.0, atol=0.01)
10391047

1040-
@pytest.mark.parametrize("solver,io_api", qc_solver_params)
1048+
@pytest.mark.parametrize("solver,io_api", nonconvex_qc_solver_params)
10411049
def test_qc_cross_terms(
10421050
self, qc_cross_terms_model: Model, solver: str, io_api: str
10431051
) -> None:
10441052
"""Test QC with cross product terms (xy) - nonconvex bilinear."""
1045-
# MOSEK and COPT do not support nonconvex problems
1046-
if solver in ("mosek", "copt"):
1047-
pytest.skip(f"{solver.upper()} does not support nonconvex bilinear constraints")
1048-
10491053
status, condition = qc_cross_terms_model.solve(solver, io_api=io_api)
10501054
assert status == "ok"
10511055
assert condition == "optimal"
@@ -1062,15 +1066,11 @@ def test_qc_cross_terms(
10621066
# Verify optimal objective value
10631067
assert np.isclose(obj_val, 5.0, atol=0.01)
10641068

1065-
@pytest.mark.parametrize("solver,io_api", qc_solver_params)
1069+
@pytest.mark.parametrize("solver,io_api", nonconvex_qc_solver_params)
10661070
def test_qc_geq_constraint(
10671071
self, qc_geq_model: Model, solver: str, io_api: str
10681072
) -> None:
10691073
"""Test >= quadratic constraint - nonconvex."""
1070-
# MOSEK and COPT do not support nonconvex problems
1071-
if solver in ("mosek", "copt"):
1072-
pytest.skip(f"{solver.upper()} does not support nonconvex >= quadratic constraints")
1073-
10741074
status, condition = qc_geq_model.solve(solver, io_api=io_api)
10751075
assert status == "ok"
10761076
assert condition == "optimal"
@@ -1087,15 +1087,11 @@ def test_qc_geq_constraint(
10871087
# Verify optimal objective value
10881088
assert np.isclose(obj_val, 2.0, atol=0.01)
10891089

1090-
@pytest.mark.parametrize("solver,io_api", qc_solver_params)
1090+
@pytest.mark.parametrize("solver,io_api", nonconvex_qc_solver_params)
10911091
def test_qc_equality_constraint(
10921092
self, qc_equality_model: Model, solver: str, io_api: str
10931093
) -> None:
10941094
"""Test = quadratic constraint - nonconvex equality."""
1095-
# MOSEK does not support nonlinear equality constraints
1096-
if solver == "mosek":
1097-
pytest.skip("MOSEK does not support nonlinear equality constraints")
1098-
10991095
status, condition = qc_equality_model.solve(solver, io_api=io_api)
11001096
assert status == "ok"
11011097
assert condition == "optimal"

0 commit comments

Comments
 (0)