Skip to content

Commit

Permalink
DQCP (cvxpy#721)
Browse files Browse the repository at this point in the history
* Initial scaffolding for dqcp

* Initial implementation of dqcp2dcp

* Prototype of bisection routine

* Solve DQCPs with problem.solve(qcp=True)

* Very basic tests for qcp

* Prototype of multiply atom

* Multiply and ratio atoms, signed canonicalization

* Fix bug in bisection interval finding, add tests

* length, lazy constraints

* Test feasibility of qcps

* add sign

* Add dist ratio

* Add max generalized eigenvalue

* Fix validation for gen_lambda_max

* fixes

* Fix bug in inverting, rename tests

* Tests

* Address comments

* Python 2 compat

* SCS -> ECOS in some tests, for windows under/overflow

* SCS -> ECOS in test
  • Loading branch information
akshayka authored and Steven Diamond committed May 7, 2019
1 parent 5bffbe5 commit 17e6bdc
Show file tree
Hide file tree
Showing 31 changed files with 1,932 additions and 56 deletions.
4 changes: 3 additions & 1 deletion copyright.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
Copyright 2017 Steven Diamond
"""
Copyright, the CVXPY authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand All @@ -11,3 +12,4 @@ distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
5 changes: 5 additions & 0 deletions cvxpy/atoms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@
limitations under the License.
"""

from cvxpy.atoms.dist_ratio import dist_ratio
from cvxpy.atoms.eye_minus_inv import eye_minus_inv, resolvent
from cvxpy.atoms.geo_mean import geo_mean
from cvxpy.atoms.gen_lambda_max import gen_lambda_max
from cvxpy.atoms.harmonic_mean import harmonic_mean
from cvxpy.atoms.lambda_max import lambda_max
from cvxpy.atoms.lambda_min import lambda_min
from cvxpy.atoms.lambda_sum_largest import lambda_sum_largest
from cvxpy.atoms.lambda_sum_smallest import lambda_sum_smallest
from cvxpy.atoms.length import length
from cvxpy.atoms.log_det import log_det
from cvxpy.atoms.log_sum_exp import log_sum_exp
from cvxpy.atoms.matrix_frac import matrix_frac
Expand All @@ -42,6 +45,7 @@
from cvxpy.atoms.quad_form import quad_form, QuadForm
from cvxpy.atoms.quad_over_lin import quad_over_lin
from cvxpy.atoms.sigma_max import sigma_max
from cvxpy.atoms.sign import sign
from cvxpy.atoms.sum_largest import sum_largest
from cvxpy.atoms.sum_smallest import sum_smallest
from cvxpy.atoms.sum_squares import sum_squares
Expand All @@ -67,6 +71,7 @@
from cvxpy.atoms.affine.vstack import vstack

from cvxpy.atoms.elementwise.abs import abs
from cvxpy.atoms.elementwise.ceil import ceil, floor
from cvxpy.atoms.elementwise.entr import entr
from cvxpy.atoms.elementwise.exp import exp
from cvxpy.atoms.elementwise.huber import huber
Expand Down
18 changes: 18 additions & 0 deletions cvxpy/atoms/affine/binary_operators.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,18 @@ def is_atom_log_log_concave(self):
"""
return True

def is_atom_quasiconvex(self):
return (
self.args[0].is_constant() or self.args[1].is_constant()) or (
self.args[0].is_nonneg() and self.args[1].is_nonpos()) or (
self.args[0].is_nonpos() and self.args[1].is_nonneg())

def is_atom_quasiconcave(self):
return (
self.args[0].is_constant() or self.args[1].is_constant()) or all(
arg.is_nonneg() for arg in self.args) or all(
arg.is_nonpos() for arg in self.args)

def numeric(self, values):
"""Multiplies the values elementwise.
"""
Expand Down Expand Up @@ -334,6 +346,12 @@ def is_atom_log_log_concave(self):
"""
return True

def is_atom_quasiconvex(self):
return self.args[1].is_nonneg() or self.args[1].is_nonpos()

def is_atom_quasiconcave(self):
return self.is_atom_quasiconvex()

def is_incr(self, idx):
"""Is the composition non-decreasing in argument idx?
"""
Expand Down
62 changes: 62 additions & 0 deletions cvxpy/atoms/atom.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

from cvxpy import utilities as u
from cvxpy import interface as intf
from cvxpy.expressions import cvxtypes
from cvxpy.expressions.constants import Constant, CallbackParam
from cvxpy.expressions.expression import Expression
import cvxpy.lin_ops.lin_utils as lu
Expand Down Expand Up @@ -133,6 +134,16 @@ def is_atom_log_log_concave(self):
"""
return False

def is_atom_quasiconvex(self):
"""Is the atom quasiconvex?
"""
return self.is_atom_convex()

def is_atom_quasiconcave(self):
"""Is the atom quasiconcave?
"""
return self.is_atom_concave()

def is_atom_log_log_affine(self):
"""Is the atom log-log affine?
"""
Expand Down Expand Up @@ -218,6 +229,57 @@ def is_log_log_concave(self):
else:
return False

@perf.compute_once
def _non_const_idx(self):
return [i for i, arg in enumerate(self.args) if not arg.is_constant()]

@perf.compute_once
def is_quasiconvex(self):
"""Is the expression quaisconvex?
"""
# Verifies the DQCP composition rule.
if self.is_convex():
return True
if type(self) == cvxtypes.maximum():
return all(arg.is_quasiconvex() for arg in self.args)
non_const = self._non_const_idx()
if self.is_scalar() and len(non_const) == 1 and self.is_incr(non_const[0]):
# TODO(akshayka): Accommodate vector atoms if people want it.
return self.args[non_const[0]].is_quasiconvex()
if self.is_scalar() and len(non_const) == 1 and self.is_decr(non_const[0]):
return self.args[non_const[0]].is_quasiconcave()
if self.is_atom_quasiconvex():
for idx, arg in enumerate(self.args):
if not (arg.is_affine() or
(arg.is_convex() and self.is_incr(idx)) or
(arg.is_concave() and self.is_decr(idx))):
return False
return True
return False

@perf.compute_once
def is_quasiconcave(self):
"""Is the expression quasiconcave?
"""
# Verifies the DQCP composition rule.
if self.is_concave():
return True
if type(self) == cvxtypes.minimum():
return all(arg.is_quasiconcave() for arg in self.args)
non_const = self._non_const_idx()
if self.is_scalar() and len(non_const) == 1 and self.is_incr(non_const[0]):
return self.args[non_const[0]].is_quasiconcave()
if self.is_scalar() and len(non_const) == 1 and self.is_decr(non_const[0]):
return self.args[non_const[0]].is_quasiconvex()
if self.is_atom_quasiconcave():
for idx, arg in enumerate(self.args):
if not (arg.is_affine() or
(arg.is_concave() and self.is_incr(idx)) or
(arg.is_convex() and self.is_decr(idx))):
return False
return True
return False

def canonicalize(self):
"""Represent the atom as an affine objective and conic constraints.
"""
Expand Down
82 changes: 82 additions & 0 deletions cvxpy/atoms/dist_ratio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""
Copyright, the CVXPY authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from cvxpy.atoms.atom import Atom
import numpy as np


class dist_ratio(Atom):
"""norm(x - a)_2 / norm(x - b)_2, with norm(x - a)_2 <= norm(x - b).
`a` and `b` must be constants.
"""
def __init__(self, x, a, b):
super(dist_ratio, self).__init__(x, a, b)
if not self.args[1].is_constant():
raise ValueError("`a` must be a constant.")
if not self.args[2].is_constant():
raise ValueError("`b` must be a constant.")
self.a = self.args[1].value
self.b = self.args[2].value

def numeric(self, values):
"""Returns the distance ratio.
"""
return np.linalg.norm(
values[0] - self.a) / np.linalg.norm(values[0] - self.b)

def shape_from_args(self):
"""Returns the (row, col) shape of the expression.
"""
return tuple()

def sign_from_args(self):
"""Returns sign (is positive, is negative) of the expression.
"""
# Always nonnegative.
return (True, False)

def is_atom_convex(self):
"""Is the atom convex?
"""
return False

def is_atom_concave(self):
"""Is the atom concave?
"""
return False

def is_atom_quasiconvex(self):
"""Is the atom quasiconvex?
"""
return True

def is_atom_quasiconcave(self):
"""Is the atom quasiconvex?
"""
return False

def is_incr(self, idx):
"""Is the composition non-decreasing in argument idx?
"""
return False

def is_decr(self, idx):
"""Is the composition non-increasing in argument idx?
"""
return False

def _grad(self, values):
return None
Loading

0 comments on commit 17e6bdc

Please sign in to comment.