Skip to content

Commit

Permalink
Add linearized versions of and and or
Browse files Browse the repository at this point in the history
  • Loading branch information
pablormier committed Apr 29, 2024
1 parent 42df18c commit f63cc90
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 21 deletions.
49 changes: 44 additions & 5 deletions corneto/backend/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ def _eq_shape(a: np.ndarray, b: np.ndarray) -> bool:
def _identical_columns(array):
# Get the first column as a reference column
ref_column = array[:, 0]

# Compare all columns to the reference column
# np.all will check if all elements in the result are True along axis 0 (down the rows)
are_columns_identical = np.all(array == ref_column[:, np.newaxis], axis=0)

# np.all on the result checks if all columns are identical to the reference column
return np.all(are_columns_identical)

Expand Down Expand Up @@ -108,7 +108,7 @@ def _norm(self, p: int = 2) -> Any:
@_delegate
def norm(self, p: int = 2) -> "CExpression":
return self._norm(p=p)

@abc.abstractmethod
def _sum(self, axis: Optional[int] = None) -> Any:
pass
Expand All @@ -117,6 +117,14 @@ def _sum(self, axis: Optional[int] = None) -> Any:
def sum(self, axis: Optional[int] = None) -> "CExpression":
return self._sum(axis=axis)

@abc.abstractmethod
def _max(self, axis: Optional[int] = None) -> Any:
pass

@_delegate
def max(self, axis: Optional[int] = None) -> "CExpression":
return self._max(axis=axis)

@_delegate
def __getitem__(self, item) -> "CExpression": # type: ignore
pass
Expand Down Expand Up @@ -722,9 +730,13 @@ def Flow(
if shared_bounds and n_flows > 1:
# check num dims of lb
if len(shape) > 1 and shape[1] > 1 and not _identical_columns(lb):
raise ValueError("shared_bounds=True cannot be used when lower bounds are not identical across flows")
raise ValueError(
"shared_bounds=True cannot be used when lower bounds are not identical across flows"
)
if len(shape) > 1 and shape[1] > 1 and not _identical_columns(ub):
raise ValueError("shared_bounds=True cannot be used when upper bounds are not identical across flows")
raise ValueError(
"shared_bounds=True cannot be used when upper bounds are not identical across flows"
)
S = F.sum(axis=1)
P += S <= ub[:, 0]
P += S >= lb[:, 0]
Expand Down Expand Up @@ -970,6 +982,33 @@ def Xor(self, x: CExpression, y: CExpression, varname="_xor"):
[xor >= x - y, xor >= y - x, xor <= x + y, xor <= 2 - x - y]
)

def linear_or(
self, x: CSymbol, axis: Optional[int] = None, varname="_linear_or"
):
# Check if the variable is binary, otherwise throw an error
if x._vartype != VarType.BINARY:
raise ValueError(f"Variable x has type {x._vartype} instead of BINARY")
Z = x.sum(axis=axis)
Z_norm = Z / x.shape[axis] # between 0-1
# Create a new binary variable to compute linearized or
Or = self.Variable(varname, Z.shape, 0, 1, vartype=VarType.BINARY)
return self.Problem([Or >= Z_norm, Or <= Z])


def linear_and(
self, x: CSymbol, axis: Optional[int] = None, varname="_linear_and"
):
# Check if the variable is binary, otherwise throw an error
if x._vartype != VarType.BINARY:
raise ValueError(f"Variable x has type {x._vartype} instead of BINARY")
Z = x.sum(axis=axis)
N = x.shape[axis]
Z_norm = Z / N
And = self.Variable(varname, Z.shape, 0, 1, vartype=VarType.BINARY)
return self.Problem([And <= Z_norm, And >= Z - N + 1])




class NoBackend(Backend):
def __init__(self) -> None:
Expand Down
9 changes: 4 additions & 5 deletions corneto/backend/_cvxpy_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,19 @@ def __init__(self, expr: Any, symbols: Optional[Set["CSymbol"]] = None) -> None:
def _create_proxy_expr(
self, expr: Any, symbols: Optional[Set["CSymbol"]] = None
) -> "CvxpyExpression":
# TODO: Move to upper class
# if symbols is not None:
# return CvxpyExpression(expr, self._proxy_symbols | symbols)
# return CvxpyExpression(expr, self._proxy_symbols)
return CvxpyExpression(expr, symbols)

def _elementwise_mul(self, other: Any) -> Any:
return cp.multiply(self._expr, other)

def _norm(self, p: int = 2) -> CExpression:
def _norm(self, p: int = 2) -> Any:
return cp.norm(self._expr, p=p)

def _sum(self, axis: Optional[int] = None) -> Any:
return cp.sum(self._expr, axis=axis)

def _max(self, axis: Optional[int] = None) -> Any:
return cp.max(self._expr, axis=axis)

@property
def value(self) -> np.ndarray:
Expand Down
12 changes: 6 additions & 6 deletions corneto/backend/_picos_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,19 @@ def __init__(self, expr: Any, symbols: Optional[Set["CSymbol"]] = None) -> None:
def _create_proxy_expr(
self, expr: Any, symbols: Optional[Set["CSymbol"]] = None
) -> "PicosExpression":
# if symbols is not None:
# return PicosExpression(expr, self._proxy_symbols | symbols)
# return PicosExpression(expr, self._proxy_symbols)
return PicosExpression(expr, symbols)

def _elementwise_mul(self, other: Any) -> Any:
return self._expr ^ other

def _norm(self, p: int = 2) -> CExpression:
return pc.expressions.exp_norm.Norm(self._expr, p=p)
return pc.Norm(self._expr, p=p)

def _sum(self, axis: Optional[int] = None) -> Any:
return pc.expressions.sum(self._expr, axis=axis)
return pc.sum(self._expr, axis=axis)

def _max(self, axis: Optional[int] = None) -> Any:
raise NotImplementedError()

@property
def value(self) -> np.ndarray:
Expand Down Expand Up @@ -97,7 +97,7 @@ def Variable(
v = pc.BinaryVariable(name, shape)
else:
v = pc.RealVariable(name, shape, lower=lb, upper=ub)
return PicosSymbol(v, name, lb, ub)
return PicosSymbol(v, name, lb, ub, vartype=vartype)

def _solve(
self,
Expand Down
101 changes: 96 additions & 5 deletions tests/test_backend.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pytest
import pathlib
import numpy as np
from corneto.backend import PicosBackend, CvxpyBackend, Backend
from corneto.backend import PicosBackend, CvxpyBackend, Backend, VarType
from corneto._graph import Graph
import cvxpy as cp

Expand Down Expand Up @@ -69,6 +69,56 @@ def test_cvxpy_convex_apply():
assert np.all(np.array(x.value) > np.array([-1e-6, 0.62, 0.36, -1e-6, -1e-6]))


def test_delegate_multiply_shape(backend):
V = backend.Variable(shape=(2, 3))
V = V.multiply(np.ones((2, 3)))
assert V.shape == (2, 3)


def test_delegate_sum_axis0_shape(backend):
V = backend.Variable(shape=(2, 3))
V = V.sum(axis=0)
if len(V.shape) == 1:
# Cvxpy assumes keepdims=False
assert V.shape == (3,)
else:
# Picos assumes keepdims=True
assert V.shape == (1, 3)


def test_delegate_sum_axis1_shape(backend):
V = backend.Variable(shape=(2, 3))
V = V.sum(axis=1)
if len(V.shape) == 1:
# Cvxpy assumes keepdims=False
assert V.shape == (2,)
else:
# Picos assumes keepdims=True
assert V.shape == (2, 1)


def test_opt_delegate_sum_axis0(backend):
x = backend.Variable("x", (2, 3))
e = x.sum(axis=0)
P = backend.Problem()
P += x <= 10
esum = e[0] + e[1] + e[2]
P.add_objectives(esum, weights=-1)
P.solve()
assert np.isclose(esum.value, 60)


def test_opt_delegate_sum_axis1(backend):
x = backend.Variable("x", (2, 3))
e = x.sum(axis=1)
P = backend.Problem()
P += x <= 10
esum = e[0] + e[1]
P.add_objectives(esum, weights=-1)
P.solve()
assert np.isclose(esum.value, 60)


def test_cexpression_name(backend):
x = backend.Variable("x")
e = x <= 10
Expand Down Expand Up @@ -116,25 +166,24 @@ def test_register(backend):
P = backend.Problem()
x = backend.Variable("x", lb=-10, ub=10)
P += x >= 0
P.register("1-x", 1-x)
P.register("1-x", 1 - x)
assert "1-x" in P.expressions


def test_register_merge(backend):
P1 = backend.Problem()
x = backend.Variable("x", lb=-10, ub=10)
P1 += x >= 0
P1.register("1-x", 1-x)
P1.register("1-x", 1 - x)
P2 = backend.Problem()
y = backend.Variable("y", lb=-10, ub=10)
P2 += y >= 0
P2.register("1-y", 1-y)
P2.register("1-y", 1 - y)
P = P1.merge(P2)
assert "1-x" in P.expressions
assert "1-y" in P.expressions



def test_symbol_only_in_objective(backend):
x = backend.Variable("x", lb=-10, ub=10)
P = backend.Problem()
Expand Down Expand Up @@ -178,6 +227,48 @@ def test_rmatmul_symbols(backend):
assert "x" in P.symbols


def test_linearized_or_axis0(backend):
P = backend.Problem()
X = backend.Variable("X", (2, 3), vartype=VarType.BINARY)
P += backend.linear_or(X, axis=0, varname="v_or")
# Force X to have at least a 1 in the first column
P += P.expr.v_or[0] == 1
P.add_objectives(sum(X[:, 0]))
P.solve()
assert np.isclose(np.sum(X[:, 0].value), 1.0)


def test_linearized_or_axis1(backend):
P = backend.Problem()
X = backend.Variable("X", (2, 3), vartype=VarType.BINARY)
P += backend.linear_or(X, axis=1, varname="v_or")
# Force X to have at least a 1 in the first row
P += P.expr.v_or[0] == 1
P.add_objectives(sum(X[0, :]))
P.solve()
assert np.isclose(np.sum(X[0, :].value), 1.0)


def test_linearized_and_axis0(backend):
P = backend.Problem()
X = backend.Variable("X", (2, 3), vartype=VarType.BINARY)
P += backend.linear_and(X, axis=0, varname="v_and")
P += P.expr.v_and[0] == 1
P.add_objectives(sum(X[:, 0]))
P.solve()
assert np.isclose(np.sum(X[:, 0].value), 2.0)


def test_linearized_and_axis1(backend):
P = backend.Problem()
X = backend.Variable("X", (2, 3), vartype=VarType.BINARY)
P += backend.linear_and(X, axis=1, varname="v_and")
P += P.expr.v_and[0] == 1
P.add_objectives(sum(X[0, :]))
P.solve()
assert np.isclose(np.sum(X[0, :].value), 3.0)


def test_undirected_flow(backend):
g = Graph()
g.add_edges([((), "A"), ("A", "B"), ("A", "C"), ("B", "D"), ("C", "D"), ("D", ())])
Expand Down

0 comments on commit f63cc90

Please sign in to comment.