Search code examples
pythoncvxpyconvex-optimization

cvxpy: Converting a nonlinear constraint to an equivalent linear constraint


Context: I'm the developer of PyPortfolioOpt, a python portfolio optimisation library, and I'm trying to allow users to add constraints to a maximum Sharpe ratio problem.

Currently, users can pass their constraints as a lambda function, e.g to make all weights greater than 1%:

ef = EfficientFrontier(mu, S)  # mu and S are expected return and covariance
ef.add_constraint(lambda w: w >= 0.01)  # example new constraint
ef.min_volatility()  # optimise with constraint

On the backend, I pass a cvxpy variable w = cp.Variable(n) to the constraint lambda function, to create a valid cvxpy constraint, then I pass this to cp.Problem and solve it.

The trouble I am having is that maximising the Sharpe ratio requires you to make a variable substitution. Constraints of the form Ax ~ b (where ~ denotes either equality or inequality) must become Ax ~ k * b where k is a nonnegative optimisation variable.

One thing I tried was to pass w / k into the lambda function. This would then result in a constraint w / k >= 0.01, which I hoped would be equivalent to w >= k * 0.01, but sadly this gives:

DCPError: Problem does not follow DCP rules. Specifically:
The following constraints are not DCP:
0.01 <= var2817 / Promote(var2818, (20,)) , because the following subexpressions are not:
|--  var2817 / Promote(var2818, (20,))

I then thought that I might be able to take the nonlinear constraint constr = (w / k >= 0.01) and multiply it by k to give k * constr = (w >= 0.01 * k), but you can't multiply constraints in cvxpy.

TL;DR: how can I convert the cvxpy constraint object (already instantiated) representing w / k >= 0.01 to a cvxpy constraint object representing w >= k * 0.01?

Or failing that, is there any way I can re-engineer this? I'd like to keep the lambda function API.


Solution

  • Perhaps there is some API for decomposing an already instantiated constraint so that I can put in a variable?

    Constraints are immutable by design. Immutability simplifies much of CVXPY’s logic.

    Why not construct a new constraint? You can certainly inspect the left and right hand sides of the constraint. Right now, that can be done by inspecting the args attribute (see https://github.com/cvxgrp/cvxpy/blob/master/cvxpy/constraints/nonpos.py#L97).