Skip to content

Commit

Permalink
Add support for norm2 (#36)
Browse files Browse the repository at this point in the history
Fairly straightforward as the implementation is the same as for the
1-norm.

However, we are starting to see small differences in results in some
tests. For now, the parameters were chosen to get a similar solution
with and without this library but changing them a little bit makes the
backfill test start to fail.

Part of #23
  • Loading branch information
jonathanberthias authored Aug 31, 2024
1 parent 89da4e4 commit d189eb7
Show file tree
Hide file tree
Showing 25 changed files with 625 additions and 77 deletions.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ dependencies = [
"coverage[toml]",
"pytest",
"pytest-insta",
"pytest-xdist",
]
matrix-name-format = "{variable}{value}"

Expand Down
22 changes: 18 additions & 4 deletions src/cvxpy_gurobi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ class InvalidPowerError(UnsupportedExpressionError):
msg_template = "Unsupported power: {node}, only quadratic expressions are supported"


class InvalidNormError(UnsupportedExpressionError):
msg_template = (
"Unsupported norm: {node}, only 1-norm, 2-norm and inf-norm are supported"
)


class _Timer:
time: float

Expand Down Expand Up @@ -599,14 +605,22 @@ def visit_multiply(self, mul: multiply) -> Any:
def visit_NegExpression(self, node: NegExpression) -> Any:
return -self.visit(node.args[0])

def visit_norm1(self, node: cp.norm1) -> Any:
def _handle_norm(self, node: cp.norm1 | cp.Pnorm, p: int, name: str) -> Any:
(x,) = node.args
if isinstance(x, cp.Constant):
return np.linalg.norm(x.value.ravel(), 1)
return np.linalg.norm(x.value.ravel(), p)
arg = self.translate_into_variable(x)
varargs = [arg] if isinstance(arg, gp.Var) else arg.reshape(-1).tolist()
norm = gp.norm(varargs, 1)
return self.make_auxilliary_variable_for(norm, "norm1", lb=0)
norm = gp.norm(varargs, p)
return self.make_auxilliary_variable_for(norm, name, lb=0)

def visit_norm1(self, node: cp.norm1) -> Any:
return self._handle_norm(node, 1, "norm1")

def visit_Pnorm(self, node: cp.Pnorm) -> Any:
if node.p != 2:
raise InvalidNormError(node)
return self._handle_norm(node, 2, "norm2")

def visit_power(self, node: power) -> Any:
power = self.visit(node.p)
Expand Down
4 changes: 2 additions & 2 deletions tests/snapshots/all__lp_genexpr_norm1_10__0.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
CVXPY
Minimize
norm1(x) + norm1([ 1. -2.])
norm1(x) + norm1([ 1. -1.])
Subject To
Bounds
x free
Expand All @@ -23,7 +23,7 @@ End
----------------------------------------
GUROBI
Minimize
0 x[0] + 0 x[1] + norm1_1 + 3 Constant
0 x[0] + 0 x[1] + norm1_1 + 2 Constant
Subject To
Bounds
x[0] free
Expand Down
12 changes: 6 additions & 6 deletions tests/snapshots/all__lp_genexpr_norm1_11__0.txt
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
CVXPY
Maximize
Sum(x @ [ 1. -2.], None, False)
Sum(x @ [ 1. -1.], None, False)
Subject To
40: norm1(x) <= 1.0
40: norm1(x) <= 1.4142135623730951
Bounds
x free
End
----------------------------------------
AFTER COMPILATION
Minimize
- C0 + 2 C1
- C0 + C1
Subject To
R0: C0 - C2 <= 0
R1: C1 - C3 <= 0
R2: - C0 - C2 <= 0
R3: - C1 - C3 <= 0
R4: C2 + C3 <= 1
R4: C2 + C3 <= 1.414213562373095
Bounds
C0 free
C1 free
Expand All @@ -25,9 +25,9 @@ End
----------------------------------------
GUROBI
Maximize
x[0] - 2 x[1]
x[0] - x[1]
Subject To
40: norm1_1 <= 1
40: norm1_1 <= 1.414213562373095
Bounds
x[0] free
x[1] free
Expand Down
22 changes: 11 additions & 11 deletions tests/snapshots/all__lp_genexpr_norm1_13__0.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
CVXPY
Minimize
norm1(X + -[[1.00 -2.00]
[3.00 -4.00]])
norm1(X + -[[2.00 1.00]
[-2.00 -4.00]])
Subject To
Bounds
X free
Expand All @@ -11,13 +11,13 @@ AFTER COMPILATION
Minimize
C0 + C1 + C2 + C3
Subject To
R0: - C0 + C4 <= 1
R1: - C1 + C5 <= 3
R2: - C2 + C6 <= -2
R0: - C0 + C4 <= 2
R1: - C1 + C5 <= -2
R2: - C2 + C6 <= 1
R3: - C3 + C7 <= -4
R4: - C0 - C4 <= -1
R5: - C1 - C5 <= -3
R6: - C2 - C6 <= 2
R4: - C0 - C4 <= -2
R5: - C1 - C5 <= 2
R6: - C2 - C6 <= -1
R7: - C3 - C7 <= 4
Bounds
C0 free
Expand All @@ -34,9 +34,9 @@ GUROBI
Minimize
norm1_2
Subject To
R0: - X[0,0] + AddExpression_1[0,0] = -1
R1: - X[0,1] + AddExpression_1[0,1] = 2
R2: - X[1,0] + AddExpression_1[1,0] = -3
R0: - X[0,0] + AddExpression_1[0,0] = -2
R1: - X[0,1] + AddExpression_1[0,1] = -1
R2: - X[1,0] + AddExpression_1[1,0] = 2
R3: - X[1,1] + AddExpression_1[1,1] = 4
Bounds
X[0,0] free
Expand Down
6 changes: 3 additions & 3 deletions tests/snapshots/all__lp_genexpr_norm1_14__0.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
CVXPY
Minimize
norm1(X) + norm1([[1.00 -2.00]
[3.00 -4.00]])
norm1(X) + norm1([[2.00 1.00]
[-2.00 -4.00]])
Subject To
Bounds
X free
Expand Down Expand Up @@ -32,7 +32,7 @@ End
----------------------------------------
GUROBI
Minimize
0 X[0,0] + 0 X[0,1] + 0 X[1,0] + 0 X[1,1] + norm1_1 + 10 Constant
0 X[0,0] + 0 X[0,1] + 0 X[1,0] + 0 X[1,1] + norm1_1 + 9 Constant
Subject To
Bounds
X[0,0] free
Expand Down
14 changes: 7 additions & 7 deletions tests/snapshots/all__lp_genexpr_norm1_15__0.txt
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
CVXPY
Maximize
Sum(X @ [[1.00 -2.00]
[3.00 -4.00]], None, False)
Sum(X @ [[2.00 1.00]
[-2.00 -4.00]], None, False)
Subject To
55: norm1(X) <= 1.0
55: norm1(X) <= 6.0
Bounds
X free
End
----------------------------------------
AFTER COMPILATION
Minimize
- C0 - 3 C1 + 2 C2 + 4 C3
- 2 C0 + 2 C1 - C2 + 4 C3
Subject To
R0: C0 - C4 <= 0
R1: C1 - C5 <= 0
Expand All @@ -20,7 +20,7 @@ Subject To
R5: - C1 - C5 <= 0
R6: - C2 - C6 <= 0
R7: - C3 - C7 <= 0
R8: C4 + C5 + C6 + C7 <= 1
R8: C4 + C5 + C6 + C7 <= 6
Bounds
C0 free
C1 free
Expand All @@ -34,9 +34,9 @@ End
----------------------------------------
GUROBI
Maximize
X[0,0] - 2 X[0,1] + 3 X[1,0] - 4 X[1,1]
2 X[0,0] + X[0,1] - 2 X[1,0] - 4 X[1,1]
Subject To
55: norm1_1 <= 1
55: norm1_1 <= 6
Bounds
X[0,0] free
X[0,1] free
Expand Down
8 changes: 4 additions & 4 deletions tests/snapshots/all__lp_genexpr_norm1_9__0.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
CVXPY
Minimize
norm1(x + -[ 1. -2.])
norm1(x + -[ 1. -1.])
Subject To
Bounds
x free
Expand All @@ -11,9 +11,9 @@ Minimize
C0 + C1
Subject To
R0: - C0 + C2 <= 1
R1: - C1 + C3 <= -2
R1: - C1 + C3 <= -1
R2: - C0 - C2 <= -1
R3: - C1 - C3 <= 2
R3: - C1 - C3 <= 1
Bounds
C0 free
C1 free
Expand All @@ -26,7 +26,7 @@ Minimize
norm1_2
Subject To
R0: - x[0] + AddExpression_1[0] = -1
R1: - x[1] + AddExpression_1[1] = 2
R1: - x[1] + AddExpression_1[1] = 1
Bounds
x[0] free
x[1] free
Expand Down
12 changes: 4 additions & 8 deletions tests/snapshots/all__lp_genexpr_norm2_0__0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,19 @@ Minimize
Subject To
R0: - x_0 + soc_t_0 = 0
R1: - x_1 + soc_x_1 = 0
R2: - x_2 + soc_x_2 = 0
qc0: [ - soc_t_0 ^2 + soc_x_1 ^2 + soc_x_2 ^2 ] <= 0
qc0: [ - soc_t_0 ^2 + soc_x_1 ^2 ] <= 0
Bounds
x_0 free
x_1 free
x_2 free
soc_x_1 free
soc_x_2 free
End
----------------------------------------
GUROBI
Minimize
0 x[0] + 0 x[1] + norm2_1
0 x + norm2_1
Subject To
Bounds
x[0] free
x[1] free
x free
General Constraints
GC0: norm2_1 = NORM ( 2 ) ( x[0] , x[1] )
GC0: norm2_1 = NORM ( 2 ) ( x )
End
35 changes: 35 additions & 0 deletions tests/snapshots/all__lp_genexpr_norm2_10__0.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
CVXPY
Minimize
Pnorm(x, 2) + Pnorm([ 1. -1.], 2)
Subject To
Bounds
x free
End
----------------------------------------
AFTER COMPILATION
Minimize
x_0
Subject To
R0: - x_0 + soc_t_0 = 0
R1: - x_1 + soc_x_1 = 0
R2: - x_2 + soc_x_2 = 0
qc0: [ - soc_t_0 ^2 + soc_x_1 ^2 + soc_x_2 ^2 ] <= 0
Bounds
x_0 free
x_1 free
x_2 free
soc_x_1 free
soc_x_2 free
End
----------------------------------------
GUROBI
Minimize
0 x[0] + 0 x[1] + norm2_1 + 1.414213562373095 Constant
Subject To
Bounds
x[0] free
x[1] free
Constant = 1
General Constraints
GC0: norm2_1 = NORM ( 2 ) ( x[0] , x[1] )
End
37 changes: 37 additions & 0 deletions tests/snapshots/all__lp_genexpr_norm2_11__0.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
CVXPY
Maximize
Sum(x @ [ 1. -1.], None, False)
Subject To
40: Pnorm(x, 2) <= 1.4142135623730951
Bounds
x free
End
----------------------------------------
AFTER COMPILATION
Minimize
- x_0 + x_1
Subject To
R0: x_2 <= 1.414213562373095
R1: - x_2 + soc_t_1 = 0
R2: - x_0 + soc_x_2 = 0
R3: - x_1 + soc_x_3 = 0
qc0: [ - soc_t_1 ^2 + soc_x_2 ^2 + soc_x_3 ^2 ] <= 0
Bounds
x_0 free
x_1 free
x_2 free
soc_x_2 free
soc_x_3 free
End
----------------------------------------
GUROBI
Maximize
x[0] - x[1]
Subject To
40: norm2_1 <= 1.414213562373095
Bounds
x[0] free
x[1] free
General Constraints
GC0: norm2_1 = NORM ( 2 ) ( x[0] , x[1] )
End
43 changes: 43 additions & 0 deletions tests/snapshots/all__lp_genexpr_norm2_12__0.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
CVXPY
Minimize
Pnorm(X, 2)
Subject To
Bounds
X free
End
----------------------------------------
AFTER COMPILATION
Minimize
x_0
Subject To
R0: - x_0 + soc_t_0 = 0
R1: - x_1 + soc_x_1 = 0
R2: - x_2 + soc_x_2 = 0
R3: - x_3 + soc_x_3 = 0
R4: - x_4 + soc_x_4 = 0
qc0: [ - soc_t_0 ^2 + soc_x_1 ^2 + soc_x_2 ^2 + soc_x_3 ^2 + soc_x_4 ^2 ]
<= 0
Bounds
x_0 free
x_1 free
x_2 free
x_3 free
x_4 free
soc_x_1 free
soc_x_2 free
soc_x_3 free
soc_x_4 free
End
----------------------------------------
GUROBI
Minimize
0 X[0,0] + 0 X[0,1] + 0 X[1,0] + 0 X[1,1] + norm2_1
Subject To
Bounds
X[0,0] free
X[0,1] free
X[1,0] free
X[1,1] free
General Constraints
GC0: norm2_1 = NORM ( 2 ) ( X[0,0] , X[0,1] , X[1,0] , X[1,1] )
End
Loading

0 comments on commit d189eb7

Please sign in to comment.