def test_psd_constraints(self): """ Test positive semi-definite constraints """ C = Variable((3, 3)) obj = Maximize(C[0, 2]) constraints = [ diag(C) == 1, C[0, 1] == 0.6, C[1, 2] == -0.3, C == C.T, C >> 0 ] prob = Problem(obj, constraints) self.assertTrue(FlipObjective().accepts(prob)) p_min = FlipObjective().apply(prob) self.assertTrue(ConeMatrixStuffing().accepts(p_min[0])) C = Variable((2, 2)) obj = Maximize(C[0, 1]) constraints = [C == 1, C >> [[2, 0], [0, 2]]] prob = Problem(obj, constraints) self.assertTrue(FlipObjective().accepts(prob)) p_min = FlipObjective().apply(prob) self.assertTrue(ConeMatrixStuffing().accepts(p_min[0])) C = Variable((2, 2), symmetric=True) obj = Minimize(C[0, 0]) constraints = [C << [[2, 0], [0, 2]]] prob, _ = CvxAttr2Constr().apply(Problem(obj, constraints)) self.assertTrue(ConeMatrixStuffing().accepts(prob))
def test_nonneg_constraints_backend(self) -> None: x = Variable(shape=(2, ), name='x') objective = Maximize(-4 * x[0] - 5 * x[1]) constr_expr = hstack( [3 - (2 * x[0] + x[1]), 3 - (x[0] + 2 * x[1]), x[0], x[1]]) constraints = [NonNeg(constr_expr)] prob = Problem(objective, constraints) self.assertFalse(ConeMatrixStuffing().accepts(prob)) self.assertTrue(FlipObjective().accepts(prob)) p_min = FlipObjective().apply(prob) self.assertTrue(ConeMatrixStuffing().accepts(p_min[0]))
def construct_intermediate_chain(problem, candidates, gp: bool = False): """ Builds a chain that rewrites a problem into an intermediate representation suitable for numeric reductions. Parameters ---------- problem : Problem The problem for which to build a chain. candidates : dict Dictionary of candidate solvers divided in qp_solvers and conic_solvers. gp : bool If True, the problem is parsed as a Disciplined Geometric Program instead of as a Disciplined Convex Program. Returns ------- Chain A Chain that can be used to convert the problem to an intermediate form. Raises ------ DCPError Raised if the problem is not DCP and `gp` is False. DGPError Raised if the problem is not DGP and `gp` is True. """ reductions = [] if len(problem.variables()) == 0: return Chain(reductions=reductions) # TODO Handle boolean constraints. if complex2real.accepts(problem): reductions += [complex2real.Complex2Real()] if gp: reductions += [Dgp2Dcp()] if not gp and not problem.is_dcp(): append = build_non_disciplined_error_msg(problem, 'DCP') if problem.is_dgp(): append += ("\nHowever, the problem does follow DGP rules. " "Consider calling solve() with `gp=True`.") elif problem.is_dqcp(): append += ("\nHowever, the problem does follow DQCP rules. " "Consider calling solve() with `qcp=True`.") raise DCPError("Problem does not follow DCP rules. Specifically:\n" + append) elif gp and not problem.is_dgp(): append = build_non_disciplined_error_msg(problem, 'DGP') if problem.is_dcp(): append += ("\nHowever, the problem does follow DCP rules. " "Consider calling solve() with `gp=False`.") elif problem.is_dqcp(): append += ("\nHowever, the problem does follow DQCP rules. " "Consider calling solve() with `qcp=True`.") raise DGPError("Problem does not follow DGP rules." + append) # Dcp2Cone and Qp2SymbolicQp require problems to minimize their objectives. if type(problem.objective) == Maximize: reductions += [FlipObjective()] # First, attempt to canonicalize the problem to a linearly constrained QP. if candidates['qp_solvers'] and qp2symbolic_qp.accepts(problem): reductions += [CvxAttr2Constr(), Qp2SymbolicQp()] return Chain(reductions=reductions) # Canonicalize it to conic problem. if not candidates['conic_solvers']: raise SolverError("Problem could not be reduced to a QP, and no " "conic solvers exist among candidate solvers " "(%s)." % candidates) reductions += [Dcp2Cone(), CvxAttr2Constr()] return Chain(reductions=reductions)
def _solve(self, solver=None, warm_start=True, verbose=False, gp=False, qcp=False, requires_grad=False, enforce_dpp=False, **kwargs): """Solves a DCP compliant optimization problem. Saves the values of primal and dual variables in the variable and constraint objects, respectively. Arguments --------- solver : str, optional The solver to use. Defaults to ECOS. warm_start : bool, optional Should the previous solver result be used to warm start? verbose : bool, optional Overrides the default of hiding solver output. gp : bool, optional If True, parses the problem as a disciplined geometric program. qcp : bool, optional If True, parses the problem as a disciplined quasiconvex program. requires_grad : bool, optional Makes it possible to compute gradients with respect to parameters by calling `backward()` after solving, or to compute perturbations to the variables by calling `derivative()`. When True, the solver must be SCS, and dqcp must be False. A DPPError is thrown when problem is not DPP. enforce_dpp : bool, optional When True, a DPPError will be thrown when trying to solve a non-DPP problem (instead of just a warning). Defaults to False. kwargs : dict, optional A dict of options that will be passed to the specific solver. In general, these options will override any default settings imposed by cvxpy. Returns ------- float The optimal value for the problem, or a string indicating why the problem could not be solved. """ for parameter in self.parameters(): if parameter.value is None: raise error.ParameterError( "A Parameter (whose name is '%s') does not have a value " "associated with it; all Parameter objects must have " "values before solving a problem." % parameter.name()) if requires_grad: dpp_context = 'dgp' if gp else 'dcp' if qcp: raise ValueError("Cannot compute gradients of DQCP problems.") elif not self.is_dpp(dpp_context): raise error.DPPError("Problem is not DPP (when requires_grad " "is True, problem must be DPP).") elif solver is not None and solver not in [s.SCS, s.DIFFCP]: raise ValueError("When requires_grad is True, the only " "supported solver is SCS " "(received %s)." % solver) elif s.DIFFCP not in slv_def.INSTALLED_SOLVERS: raise ImportError( "The Python package diffcp must be installed to " "differentiate through problems. Please follow the " "installation instructions at " "https://github.com/cvxgrp/diffcp") else: solver = s.DIFFCP else: if gp and qcp: raise ValueError("At most one of `gp` and `qcp` can be True.") if qcp and not self.is_dcp(): if not self.is_dqcp(): raise error.DQCPError("The problem is not DQCP.") reductions = [dqcp2dcp.Dqcp2Dcp()] if type(self.objective) == Maximize: reductions = [FlipObjective()] + reductions chain = Chain(problem=self, reductions=reductions) soln = bisection.bisect(chain.reduce(), solver=solver, verbose=verbose, **kwargs) self.unpack(chain.retrieve(soln)) return self.value data, solving_chain, inverse_data = self.get_problem_data( solver, gp, enforce_dpp) solution = solving_chain.solve_via_data(self, data, warm_start, verbose, kwargs) self.unpack_results(solution, solving_chain, inverse_data) return self.value
def test_scalar_lp(self): """Test scalar LP problems. """ for solver in self.solvers: p = Problem(Minimize(3 * self.a), [self.a >= 2]) self.assertTrue(ConeMatrixStuffing().accepts(p)) result = p.solve(solver.name()) p_new = ConeMatrixStuffing().apply(p) result_new = p_new[0].solve(solver.name()) self.assertAlmostEqual(result, result_new) sltn = solver.solve(p_new[0], False, False, {}) self.assertAlmostEqual(sltn.opt_val, result) inv_sltn = ConeMatrixStuffing().invert(sltn, p_new[1]) self.assertAlmostEqual(inv_sltn.opt_val, result) self.assertAlmostEqual(inv_sltn.primal_vars[self.a.id], self.a.value) # TODO: Maximize p = Problem(Minimize(-3 * self.a + self.b), [self.a <= 2, self.b == self.a, self.b <= 5]) result = p.solve(solver.name()) self.assertTrue(ConeMatrixStuffing().accepts(p)) p_new = ConeMatrixStuffing().apply(p) sltn = solver.solve(p_new[0], False, False, {}) self.assertAlmostEqual(sltn.opt_val, result) inv_sltn = ConeMatrixStuffing().invert(sltn, p_new[1]) self.assertAlmostEqual(inv_sltn.opt_val, result) self.assertAlmostEqual(inv_sltn.primal_vars[self.a.id], self.a.value) self.assertAlmostEqual(inv_sltn.primal_vars[self.b.id], self.b.value) # With a constant in the objective. p = Problem(Minimize(3 * self.a - self.b + 100), [ self.a >= 2, self.b + 5 * self.c - 2 == self.a, self.b <= 5 + self.c ]) self.assertTrue(ConeMatrixStuffing().accepts(p)) result = p.solve(solver.name()) p_new = ConeMatrixStuffing().apply(p) sltn = solver.solve(p_new[0], False, False, {}) self.assertAlmostEqual(sltn.opt_val, result - 100) inv_sltn = ConeMatrixStuffing().invert(sltn, p_new[1]) self.assertAlmostEqual(inv_sltn.opt_val, result) self.assertAlmostEqual(inv_sltn.primal_vars[self.a.id], self.a.value) self.assertAlmostEqual(inv_sltn.primal_vars[self.b.id], self.b.value) # Unbounded problems. # TODO: Maximize p = Problem(Minimize(-self.a), [self.a >= 2]) self.assertTrue(ConeMatrixStuffing().accepts(p)) try: result = p.solve(solver.name()) except SolverError: # Gurobi fails on this one return p_new = ConeMatrixStuffing().apply(p) self.assertTrue(solver.accepts(p_new[0])) sltn = solver.solve(p_new[0], False, False, {}) self.assertAlmostEqual(sltn.opt_val, result) inv_sltn = ConeMatrixStuffing().invert(sltn, p_new[1]) self.assertAlmostEqual(inv_sltn.opt_val, result) # Infeasible problems. p = Problem(Maximize(self.a), [self.a >= 2, self.a <= 1]) result = p.solve(solver.name()) self.assertTrue(FlipObjective().accepts(p)) p_min = FlipObjective().apply(p) self.assertTrue(ConeMatrixStuffing().accepts(p_min[0])) p_new = ConeMatrixStuffing().apply(p_min[0]) result_new = p_new[0].solve(solver.name()) self.assertAlmostEqual(result, -result_new) self.assertTrue(solver.accepts(p_new[0])) sltn = solver.solve(p_new[0], False, False, {}) self.assertAlmostEqual(sltn.opt_val, -result) inv_sltn = ConeMatrixStuffing().invert(sltn, p_new[1]) self.assertAlmostEqual(inv_sltn.opt_val, -result) inv_flipped_sltn = FlipObjective().invert(inv_sltn, p_min[1]) self.assertAlmostEqual(inv_flipped_sltn.opt_val, result)
def _reductions_for_problem_class(problem, candidates, gp: bool = False) -> List[Any]: """ Builds a chain that rewrites a problem into an intermediate representation suitable for numeric reductions. Parameters ---------- problem : Problem The problem for which to build a chain. candidates : dict Dictionary of candidate solvers divided in qp_solvers and conic_solvers. gp : bool If True, the problem is parsed as a Disciplined Geometric Program instead of as a Disciplined Convex Program. Returns ------- list of Reduction objects A list of reductions that can be used to convert the problem to an intermediate form. Raises ------ DCPError Raised if the problem is not DCP and `gp` is False. DGPError Raised if the problem is not DGP and `gp` is True. """ reductions = [] # TODO Handle boolean constraints. if complex2real.accepts(problem): reductions += [complex2real.Complex2Real()] if gp: reductions += [Dgp2Dcp()] if not gp and not problem.is_dcp(): append = build_non_disciplined_error_msg(problem, 'DCP') if problem.is_dgp(): append += ("\nHowever, the problem does follow DGP rules. " "Consider calling solve() with `gp=True`.") elif problem.is_dqcp(): append += ("\nHowever, the problem does follow DQCP rules. " "Consider calling solve() with `qcp=True`.") raise DCPError("Problem does not follow DCP rules. Specifically:\n" + append) elif gp and not problem.is_dgp(): append = build_non_disciplined_error_msg(problem, 'DGP') if problem.is_dcp(): append += ("\nHowever, the problem does follow DCP rules. " "Consider calling solve() with `gp=False`.") elif problem.is_dqcp(): append += ("\nHowever, the problem does follow DQCP rules. " "Consider calling solve() with `qcp=True`.") raise DGPError("Problem does not follow DGP rules." + append) # Dcp2Cone and Qp2SymbolicQp require problems to minimize their objectives. if type(problem.objective) == Maximize: reductions += [FlipObjective()] if _solve_as_qp(problem, candidates): reductions += [CvxAttr2Constr(), qp2symbolic_qp.Qp2SymbolicQp()] else: # Canonicalize it to conic problem. if not candidates['conic_solvers']: raise SolverError("Problem could not be reduced to a QP, and no " "conic solvers exist among candidate solvers " "(%s)." % candidates) else: reductions += [Dcp2Cone(), CvxAttr2Constr()] constr_types = {type(c) for c in problem.constraints} if FiniteSet in constr_types: reductions += [Valinvec2mixedint()] return reductions
def _solve(self, solver=None, warm_start=True, verbose=False, parallel=False, gp=False, qcp=False, **kwargs): """Solves a DCP compliant optimization problem. Saves the values of primal and dual variables in the variable and constraint objects, respectively. Parameters ---------- solver : str, optional The solver to use. Defaults to ECOS. warm_start : bool, optional Should the previous solver result be used to warm start? verbose : bool, optional Overrides the default of hiding solver output. parallel : bool, optional If problem is separable, solve in parallel. gp : bool, optional If True, parses the problem as a disciplined geometric program. qcp : bool, optional If True, parses the problem as a disciplined quasiconvex program. kwargs : dict, optional A dict of options that will be passed to the specific solver. In general, these options will override any default settings imposed by cvxpy. Returns ------- float The optimal value for the problem, or a string indicating why the problem could not be solved. """ if gp and qcp: raise ValueError("At most one of `gp` and `qcp` can be True.") if qcp and not self.is_dcp(): if not self.is_dqcp(): raise error.DQCPError("The problem is not DQCP.") reductions = [dqcp2dcp.Dqcp2Dcp()] if type(self.objective) == Maximize: reductions = [FlipObjective()] + reductions chain = Chain(problem=self, reductions=reductions) soln = bisection.bisect(chain.reduce(), solver=solver, verbose=verbose, **kwargs) self.unpack(chain.retrieve(soln)) return self.value if parallel: from cvxpy.transforms.separable_problems import get_separable_problems self._separable_problems = (get_separable_problems(self)) if len(self._separable_problems) > 1: return self._parallel_solve(solver, warm_start, verbose, **kwargs) self._construct_chains(solver=solver, gp=gp) data, solving_inverse_data = self._solving_chain.apply( self._intermediate_problem) solution = self._solving_chain.solve_via_data(self, data, warm_start, verbose, kwargs) full_chain = self._solving_chain.prepend(self._intermediate_chain) inverse_data = self._intermediate_inverse_data + solving_inverse_data self.unpack_results(solution, full_chain, inverse_data) return self.value