Reputation: 61289
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
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