def _add_custom_solver_candidates(self, custom_solver: Solver): """ Returns a list of candidate solvers where custom_solver is the only potential option. Arguments --------- custom_solver : Solver Returns ------- dict A dictionary of compatible solvers divided in `qp_solvers` and `conic_solvers`. Raises ------ cvxpy.error.SolverError Raised if the name of the custom solver conflicts with the name of some officially supported solver """ if custom_solver.name() in SOLVERS: message = "Custom solvers must have a different name than the officially supported ones" raise(error.SolverError(message)) candidates = {'qp_solvers': [], 'conic_solvers': []} if not self.is_mixed_integer() or custom_solver.MIP_CAPABLE: if isinstance(custom_solver, QpSolver): SOLVER_MAP_QP[custom_solver.name()] = custom_solver candidates['qp_solvers'] = [custom_solver.name()] elif isinstance(custom_solver, ConicSolver): SOLVER_MAP_CONIC[custom_solver.name()] = custom_solver candidates['conic_solvers'] = [custom_solver.name()] return candidates
def unpack_results(self, solution, chain, inverse_data): """Updates the problem state given the solver results. Updates problem.status, problem.value and value of primal and dual variables. Arguments _________ solution : object The solution returned by applying the chain to the problem and invoking the solver on the resulting data. chain : SolvingChain A solving chain that was used to solve the problem. inverse_data : list The inverse data returned by applying the chain to the problem. Raises ------ cvxpy.error.SolverError If the solver failed """ solution = chain.invert(solution, inverse_data) if solution.status in s.ERROR: raise error.SolverError( "Solver '%s' failed. " % chain.solver.name() + "Try another solver, or solve with verbose=True for more " "information.") self.unpack(solution) self._solver_stats = SolverStats(self._solution.attr, chain.solver.name())
def _bisect(problem, solver, t, low, high, tighten_lower, tighten_higher, eps=1e-6, verbose=False, max_iters=100): """Bisect `problem` on the parameter `t`.""" verbose_freq = 5 soln = None for i in range(max_iters): assert low <= high if soln is not None and (high - low) <= eps: # the previous iteration might have been infeasible, but # the tigthen* functions might have narrowed the interval # to the optimal value in the previous iteration (hence the # soln is not None check) return soln, low, high query_pt = (low + high) / 2.0 if verbose and i % verbose_freq == 0: print("(iteration %d) lower bound: %0.6f" % (i, low)) print("(iteration %d) upper bound: %0.6f" % (i, high)) print("(iteration %d) query point: %0.6f " % (i, query_pt)) t.value = query_pt lowered = _lower_problem(problem) _solve(lowered, solver=solver) if _infeasible(lowered): if verbose and i % verbose_freq == 0: print("(iteration %d) query was infeasible.\n" % i) low = tighten_lower(query_pt) elif lowered.status in s.SOLUTION_PRESENT: if verbose and i % verbose_freq == 0: print("(iteration %d) query was feasible. %s)\n" % (i, lowered.solution)) soln = lowered.solution high = tighten_higher(query_pt) else: if verbose: print("Aborting; the solver failed ...\n") raise error.SolverError("Solver failed with status %s" % lowered.status) raise error.SolverError("Max iters hit during bisection.")
def _find_bisection_interval(problem, t, solver=None, low=None, high=None, max_iters=100): """Finds an interval for bisection.""" if low is None: low = 0 if t.is_nonneg() else -1 if high is None: high = 0 if t.is_nonpos() else 1 infeasible_low = t.is_nonneg() feasible_high = t.is_nonpos() for _ in range(max_iters): if not feasible_high: t.value = high lowered = _lower_problem(problem) _solve(lowered, solver) if _infeasible(lowered): low = high high *= 2 continue elif lowered.status in s.SOLUTION_PRESENT: feasible_high = True else: raise error.SolverError("Solver failed with status %s" % lowered.status) if not infeasible_low: t.value = low lowered = _lower_problem(problem) _solve(lowered, solver=solver) if _infeasible(lowered): infeasible_low = True elif lowered.status in s.SOLUTION_PRESENT: high = low low *= 2 continue else: raise error.SolverError("Solver failed with status %s" % lowered.status) if infeasible_low and feasible_high: return low, high raise error.SolverError("Unable to find suitable interval for bisection.")
def backward(self): """Compute the gradient of a solution with respect to Parameters. This method differentiates through the solution map of the problem, obtaining the gradient of a solution with respect to the Parameters. In other words, it calculates the sensitivities of the Parameters with respect to perturbations in the optimal Variable values. This can be useful for integrating CVXPY into automatic differentation toolkits. ``backward()`` populates the ``gradient`` attribute of each Parameter in the problem as a side-effect. It can only be called after calling ``solve()`` with ``requires_grad=True``. Below is a simple example: :: import cvxpy as cp import numpy as np p = cp.Parameter() x = cp.Variable() quadratic = cp.square(x - 2 * p) problem = cp.Problem(cp.Minimize(quadratic), [x >= 0]) p.value = 3.0 problem.solve(requires_grad=True, eps=1e-10) # backward() populates the gradient attribute of the parameters problem.backward() # Because x* = 2 * p, dx*/dp = 2 np.testing.assert_allclose(p.gradient, 2.0) In the above example, the gradient could easily be computed by hand. The ``backward()`` is useful because for almost all problems, the gradient cannot be computed analytically. This method can be used to differentiate through any DCP or DGP problem, as long as the problem is DPP compliant (i.e., ``problem.is_dcp(dpp=True)`` or ``problem.is_dgp(dpp=True)`` evaluates to ``True``). This method uses the chain rule to evaluate the gradients of a scalar-valued function of the Variables with respect to the Parameters. For example, let x be a variable and p a Parameter; x and p might be scalars, vectors, or matrices. Let f be a scalar-valued function, with z = f(x). Then this method computes dz/dp = (dz/dx) (dx/p). dz/dx is chosen as the all-ones vector by default, corresponding to choosing f to be the sum function. You can specify a custom value for dz/dx by setting the ``gradient`` attribute on your variables. For example, :: import cvxpy as cp import numpy as np b = cp.Parameter() x = cp.Variable() quadratic = cp.square(x - 2 * b) problem = cp.Problem(cp.Minimize(quadratic), [x >= 0]) b.value = 3. problem.solve(requires_grad=True, eps=1e-10) x.gradient = 4. problem.backward() # dz/dp = dz/dx dx/dp = 4. * 2. == 8. np.testing.assert_allclose(b.gradient, 8.) The ``gradient`` attribute on a variable can also be interpreted as a perturbation to its optimal value. Raises ------ ValueError if solve was not called with ``requires_grad=True`` SolverError if the problem is infeasible or unbounded """ if s.DIFFCP not in self._solver_cache: raise ValueError("backward can only be called after calling " "solve with `requires_grad=True`") elif self.status not in s.SOLUTION_PRESENT: raise error.SolverError("Backpropagating through " "infeasible/unbounded problems is not " "yet supported. Please file an issue on " "Github if you need this feature.") # TODO(akshayka): Backpropagate through dual variables as well. backward_cache = self._solver_cache[s.DIFFCP] DT = backward_cache["DT"] zeros = np.zeros(backward_cache["s"].shape) del_vars = {} gp = self._cache.gp() for variable in self.variables(): if variable.gradient is None: del_vars[variable.id] = np.ones(variable.shape) else: del_vars[variable.id] = np.asarray(variable.gradient, dtype=np.float64) if gp: # x_gp = exp(x_cone_program), # dx_gp/d x_cone_program = exp(x_cone_program) = x_gp del_vars[variable.id] *= variable.value dx = self._cache.param_prog.split_adjoint(del_vars) start = time.time() dA, db, dc = DT(dx, zeros, zeros) end = time.time() backward_cache['DT_TIME'] = end - start dparams = self._cache.param_prog.apply_param_jac(dc, -dA, db) if not gp: for param in self.parameters(): param.gradient = dparams[param.id] else: dgp2dcp = self._cache.solving_chain.get(Dgp2Dcp) old_params_to_new_params = dgp2dcp.canon_methods._parameters for param in self.parameters(): # Note: if param is an exponent in a power or gmatmul atom, # then the parameter passes through unchanged to the DCP # program; if the param is also used elsewhere (not as an # exponent), then param will also be in # old_params_to_new_params. Therefore, param.gradient = # dparams[param.id] (or 0) + 1/param*dparams[new_param.id] # # Note that param.id is in dparams if and only if # param was used as an exponent (because this means that # the parameter entered the DCP problem unchanged.) grad = 0.0 if param.id not in dparams else dparams[param.id] if param in old_params_to_new_params: new_param = old_params_to_new_params[param] # new_param.value == log(param), apply chain rule grad += (1.0 / param.value) * dparams[new_param.id] param.gradient = grad
def _find_candidate_solvers(self, solver=None, gp=False): """ Find candiate solvers for the current problem. If solver is not None, it checks if the specified solver is compatible with the problem passed. Arguments --------- solver : string The name of the solver with which to solve the problem. If no solver is supplied (i.e., if solver is None), then the targeted solver may be any of those that are installed. If the problem is variable-free, then this parameter is ignored. gp : bool If True, the problem is parsed as a Disciplined Geometric Program instead of as a Disciplined Convex Program. Returns ------- dict A dictionary of compatible solvers divided in `qp_solvers` and `conic_solvers`. Raises ------ cvxpy.error.SolverError Raised if the problem is not DCP and `gp` is False. cvxpy.error.DGPError Raised if the problem is not DGP and `gp` is True. """ candidates = {'qp_solvers': [], 'conic_solvers': []} if solver is not None: if solver not in slv_def.INSTALLED_SOLVERS: raise error.SolverError("The solver %s is not installed." % solver) if solver in slv_def.CONIC_SOLVERS: candidates['conic_solvers'] += [solver] if solver in slv_def.QP_SOLVERS: candidates['qp_solvers'] += [solver] else: candidates['qp_solvers'] = [ s for s in slv_def.INSTALLED_SOLVERS if s in slv_def.QP_SOLVERS ] candidates['conic_solvers'] = [ s for s in slv_def.INSTALLED_SOLVERS if s in slv_def.CONIC_SOLVERS ] # If gp we must have only conic solvers if gp: if solver is not None and solver not in slv_def.CONIC_SOLVERS: raise error.SolverError( "When `gp=True`, `solver` must be a conic solver " "(received '%s'); try calling " % solver + " `solve()` with `solver=cvxpy.ECOS`.") elif solver is None: candidates['qp_solvers'] = [] # No QP solvers allowed if self.is_mixed_integer(): if len(slv_def.INSTALLED_MI_SOLVERS) == 0: msg = """ CVXPY needs additional software (a `mixed-integer solver`) to handle this model. The web documentation https://www.cvxpy.org/tutorial/advanced/index.html#mixed-integer-programs reviews open-source and commercial options for mixed-integer solvers. Quick fix: if you install the python package CVXOPT (pip install cvxopt), then CVXPY can use the open-source mixed-integer solver `GLPK`. """ raise error.SolverError(msg) candidates['qp_solvers'] = [ s for s in candidates['qp_solvers'] if slv_def.SOLVER_MAP_QP[s].MIP_CAPABLE ] candidates['conic_solvers'] = [ s for s in candidates['conic_solvers'] if slv_def.SOLVER_MAP_CONIC[s].MIP_CAPABLE ] if not candidates['conic_solvers'] and \ not candidates['qp_solvers']: raise error.SolverError( "Problem is mixed-integer, but candidate " "QP/Conic solvers (%s) are not MIP-capable." % (candidates['qp_solvers'] + candidates['conic_solvers'])) return candidates
def backward(self): """Compute the gradient of a solution with respect to parameters. This method differentiates through the solution map of the problem, to obtain the gradient of a solution with respect to the parameters. In other words, it calculates the sensitivities of the parameters with respect to perturbations in the optimal variable values. .backward() populates the .gradient attribute of each parameter as a side-effect. It can only be called after calling .solve() with `requires_grad=True`. Below is a simple example: :: import cvxpy as cp import numpy as np p = cp.Parameter() x = cp.Variable() quadratic = cp.square(x - 2 * p) problem = cp.Problem(cp.Minimize(quadratic), [x >= 0]) p.value = 3.0 problem.solve(requires_grad=True, eps=1e-10) # .backward() populates the .gradient attribute of the parameters problem.backward() # Because x* = 2 * p, dx*/dp = 2 np.testing.assert_allclose(p.gradient, 2.0) In the above example, the gradient could easily be computed by hand; however, .backward() can be used to differentiate through any DCP program (that is also DPP-compliant). This method uses the chain rule to evaluate the gradients of a scalar-valued function of the variables with respect to the parameters. For example, let x be a variable and p a parameter; x and p might be scalars, vectors, or matrices. Let f be a scalar-valued function, with z = f(x). Then this method computes dz/dp = (dz/dx) (dx/p). dz/dx is chosen to be the all ones vector by default, corresponding to choosing f to be the sum function. You can specify a custom value for dz/dx by setting the .gradient attribute on your variables. For example, :: import cvxpy as cp import numpy as np b = cp.Parameter() x = cp.Variable() quadratic = cp.square(x - 2 * b) problem = cp.Problem(cp.Minimize(quadratic), [x >= 0]) b.value = 3. problem.solve(requires_grad=True, eps=1e-10) x.gradient = 4. problem.backward() # dz/dp = dz/dx dx/dp = 4. * 2. == 8. np.testing.assert_allclose(b.gradient, 8.) The .gradient attribute on a variable can also be interpreted as a perturbation to its optimal value. Raises ------ ValueError if solve was not called with `requires_grad=True` SolverError if the problem is infeasible or unbounded """ if s.DIFFCP not in self._solver_cache: raise ValueError("backward can only be called after calling " "solve with `requires_grad=True`") elif self.status not in s.SOLUTION_PRESENT: raise error.SolverError("Backpropagating through " "infeasible/unbounded problems is not " "yet supported. Please file an issue on " "Github if you need this feature.") # TODO(akshayka): Backpropagate through dual variables as well. backward_cache = self._solver_cache[s.DIFFCP] DT = backward_cache["DT"] zeros = np.zeros(backward_cache["s"].shape) del_vars = {} for variable in self.variables(): if variable.gradient is None: del_vars[variable.id] = np.ones(variable.shape) else: del_vars[variable.id] = np.asarray(variable.gradient, dtype=np.float64) dx = self._cache.param_cone_prog.split_adjoint(del_vars) dA, db, dc = DT(dx, zeros, zeros) dparams = self._cache.param_cone_prog.apply_param_jac(dc, -dA, db) for parameter in self.parameters(): parameter.gradient = dparams[parameter.id]
def _find_candidate_solvers(self, solver=None, gp=False): """ Find candiate solvers for the current problem. If solver is not None, it checks if the specified solver is compatible with the problem passed. Parameters ---------- solver : string The name of the solver with which to solve the problem. If no solver is supplied (i.e., if solver is None), then the targeted solver may be any of those that are installed. If the problem is variable-free, then this parameter is ignored. gp : bool If True, the problem is parsed as a Disciplined Geometric Program instead of as a Disciplined Convex Program. Returns ------- dict A dictionary of compatible solvers divided in `qp_solvers` and `conic_solvers`. Raises ------ cvxpy.error.SolverError Raised if the problem is not DCP and `gp` is False. cvxpy.error.DGPError Raised if the problem is not DGP and `gp` is True. """ candidates = {'qp_solvers': [], 'conic_solvers': []} if solver is not None: if solver not in slv_def.INSTALLED_SOLVERS: raise error.SolverError("The solver %s is not installed." % solver) if solver in slv_def.CONIC_SOLVERS: candidates['conic_solvers'] += [solver] if solver in slv_def.QP_SOLVERS: candidates['qp_solvers'] += [solver] else: candidates['qp_solvers'] = [s for s in slv_def.INSTALLED_SOLVERS if s in slv_def.QP_SOLVERS] candidates['conic_solvers'] = [s for s in slv_def.INSTALLED_SOLVERS if s in slv_def.CONIC_SOLVERS] # If gp we must have only conic solvers if gp: if solver is not None and solver not in slv_def.CONIC_SOLVERS: raise error.SolverError( "When `gp=True`, `solver` must be a conic solver " "(received '%s'); try calling " % solver + " `solve()` with `solver=cvxpy.ECOS`." ) elif solver is None: candidates['qp_solvers'] = [] # No QP solvers allowed if self.is_mixed_integer(): candidates['qp_solvers'] = [ s for s in candidates['qp_solvers'] if slv_def.SOLVER_MAP_QP[s].MIP_CAPABLE] candidates['conic_solvers'] = [ s for s in candidates['conic_solvers'] if slv_def.SOLVER_MAP_CONIC[s].MIP_CAPABLE] if not candidates['conic_solvers'] and \ not candidates['qp_solvers']: raise error.SolverError( "Problem is mixed-integer, but candidate " "QP/Conic solvers (%s) are not MIP-capable." % [candidates['qp_solvers'], candidates['conic_solvers']]) return candidates
def _find_candidate_solvers(self, solver=None, gp=False): """ Find candidate solvers for the current problem. If solver is not None, it checks if the specified solver is compatible with the problem passed. Arguments --------- solver : Union[string, Solver, None] The name of the solver with which to solve the problem or an instance of a custom solver. If no solver is supplied (i.e., if solver is None), then the targeted solver may be any of those that are installed. If the problem is variable-free, then this parameter is ignored. gp : bool If True, the problem is parsed as a Disciplined Geometric Program instead of as a Disciplined Convex Program. Returns ------- dict A dictionary of compatible solvers divided in `qp_solvers` and `conic_solvers`. Raises ------ cvxpy.error.SolverError Raised if the problem is not DCP and `gp` is False. cvxpy.error.DGPError Raised if the problem is not DGP and `gp` is True. """ candidates = {'qp_solvers': [], 'conic_solvers': []} if isinstance(solver, Solver): return self._add_custom_solver_candidates(solver) if solver is not None: if solver not in slv_def.INSTALLED_SOLVERS: raise error.SolverError("The solver %s is not installed." % solver) if solver in slv_def.CONIC_SOLVERS: candidates['conic_solvers'] += [solver] if solver in slv_def.QP_SOLVERS: candidates['qp_solvers'] += [solver] else: candidates['qp_solvers'] = [s for s in slv_def.INSTALLED_SOLVERS if s in slv_def.QP_SOLVERS] candidates['conic_solvers'] = [] # ECOS_BB can only be called explicitly. for slv in slv_def.INSTALLED_SOLVERS: if slv in slv_def.CONIC_SOLVERS and slv != s.ECOS_BB: candidates['conic_solvers'].append(slv) # If gp we must have only conic solvers if gp: if solver is not None and solver not in slv_def.CONIC_SOLVERS: raise error.SolverError( "When `gp=True`, `solver` must be a conic solver " "(received '%s'); try calling " % solver + " `solve()` with `solver=cvxpy.ECOS`." ) elif solver is None: candidates['qp_solvers'] = [] # No QP solvers allowed if self.is_mixed_integer(): # ECOS_BB must be called explicitly. if slv_def.INSTALLED_MI_SOLVERS == [s.ECOS_BB] and solver != s.ECOS_BB: msg = """ You need a mixed-integer solver for this model. Refer to the documentation https://www.cvxpy.org/tutorial/advanced/index.html#mixed-integer-programs for discussion on this topic. Quick fix 1: if you install the python package CVXOPT (pip install cvxopt), then CVXPY can use the open-source mixed-integer solver `GLPK`. Quick fix 2: you can explicitly specify solver='ECOS_BB'. This may result in incorrect solutions and is not recommended. """ raise error.SolverError(msg) candidates['qp_solvers'] = [ s for s in candidates['qp_solvers'] if slv_def.SOLVER_MAP_QP[s].MIP_CAPABLE] candidates['conic_solvers'] = [ s for s in candidates['conic_solvers'] if slv_def.SOLVER_MAP_CONIC[s].MIP_CAPABLE] if not candidates['conic_solvers'] and \ not candidates['qp_solvers']: raise error.SolverError( "Problem is mixed-integer, but candidate " "QP/Conic solvers (%s) are not MIP-capable." % (candidates['qp_solvers'] + candidates['conic_solvers'])) return candidates