Richard
Richard

Reputation: 61289

How to use a parameter and its reciprocal in a CVXPY DPP?

In the following test program

import cvxpy as cp

def cp_log_ratio_norm(a, b):
  # Both `a * cp.inv_pos(b)` and `a / b` make this problem non-DPP
  return cp.maximum(a * b, b * cp.inv_pos(a))

var = cp.Variable(pos=True)
param = cp.Parameter(pos=True)
param.value = 5
objective = cp.Minimize(cp_log_ratio_norm(var, param))
problem = cp.Problem(objective, [])
objective_value = problem.solve()

print(f"Objective value = {objective_value}")
print(f"Status = {problem.status}")

we see that using the reciprocal of a parameter causes DPP (disciplined parameterized programming) to break with the message:

miniconda3/envs/faster-unmixer/lib/python3.10/site-packages/cvxpy/reductions/solvers/solving_chain.py:178: UserWarning: You are solving a parameterized problem that is not DPP. Because the problem is not DPP, subsequent solves will not be faster than the first one. For more information, see the documentation on Discplined Parametrized Programming, at
    https://www.cvxpy.org/tutorial/advanced/index.html#disciplined-parametrized-programming

How can I make this work?

Upvotes: 1

Views: 609

Answers (1)

Richard
Richard

Reputation: 61289

Looking at the docs we find that this is expected:

As another example, the quotient expr / p is not DPP-compliant when p is a parameter, but this can be rewritten as expr * p_tilde, where p_tilde is a parameter that represents 1/p.

But in your case we need both p and 1 / p? Keeping those two synchronized is challenging. We can use a class we'll call ReciprocalParameter to make it easier.

import cvxpy as cp
from typing import Optional

class ReciprocalParameter:
    """Used for times when you want a cvxpy Parameter and its ratio"""

    def __init__(self, *args, **kwargs) -> None:
        self._p = cp.Parameter(*args, **kwargs)
        # Reciprocal of the above
        self._rp = cp.Parameter(*args, **kwargs)

    @property
    def value(self) -> Optional[float]:
        """Return the value of the Parameter"""
        return self._p.value

    @value.setter
    def value(self, val: Optional[float]) -> None:
        """
        Simultaneously set the value of the Parameter (given by `p`)
        and its reciprocal (given by `rp`)
        """
        self._p.value = val
        self._rp.value = 1 / val if val is not None else None

    @property
    def p(self) -> cp.Parameter:
        """Returns the parameter"""
        return self._p

    @property
    def rp(self) -> cp.Parameter:
        """Returns the reciprocal of the parameter"""
        return self._rp

def cp_log_ratio_norm(a, b: ReciprocalParameter):
  # Both `a * cp.inv_pos(b)` and `a / b` make this problem non-DPP
  return cp.maximum(a * b.rp, b.p * cp.inv_pos(a))

var = cp.Variable(pos=True)
param = ReciprocalParameter(pos=True)
param.value = 5
objective = cp.Minimize(cp_log_ratio_norm(var, param))
problem = cp.Problem(objective, [])
objective_value = problem.solve()

print(f"Objective value = {objective_value}")
print(f"Status = {problem.status}")

Note that we treat ReciprocalParameter just like a normal cvxpy.Parameter until the point of use when we are forced to decide whether we want the parameter's value (ReciprocalParameter.p) or its reciprocal (ReciprocalParameter.rp).

Upvotes: 1

Related Questions