def _canonicalize_constraint(self, constr): """Recursively canonicalize a constraint.""" lhs = constr.args[0] rhs = constr.args[1] if isinstance(constr, Inequality): lhs_val = np.array(lhs.value) rhs_val = np.array(rhs.value) if np.all(lhs_val == -np.inf) or np.all(rhs_val == np.inf): # constraint is redundant return [None] elif np.any(lhs_val == np.inf) or np.any(rhs_val == -np.inf): return [s.INFEASIBLE] if constr.is_dcp(): canon_constr, aux_constr = self.canonicalize_tree(constr) return [canon_constr] + aux_constr # canonicalize lhs <= rhs # either lhs or rhs is quasiconvex (and not convex) assert isinstance(constr, Inequality) # short-circuit zero-valued expressions to simplify inverse logic if lhs.is_zero(): return self._canonicalize_constraint(0 <= rhs) if rhs.is_zero(): return self._canonicalize_constraint(lhs <= 0) if lhs.is_quasiconvex() and not lhs.is_convex(): # quasiconvex <= constant assert rhs.is_constant(), rhs if inverse.invertible(lhs): # Apply inverse to both sides of constraint. rhs = inverse.inverse(lhs)(rhs) idx = lhs._non_const_idx()[0] expr = lhs.args[idx] if lhs.is_incr(idx): return self._canonicalize_constraint(expr <= rhs) return self._canonicalize_constraint(expr >= rhs) elif isinstance(lhs, (maximum, max_atom)): # Lower maximum. return [ c for arg in lhs.args for c in self._canonicalize_constraint(arg <= rhs) ] else: # Replace quasiconvex atom with a sublevel set. canon_args, aux_args_constr = self._canon_args(lhs) sublevel_set = sets.sublevel(lhs.copy(canon_args), t=rhs) return sublevel_set + aux_args_constr # constant <= quasiconcave assert rhs.is_quasiconcave() assert lhs.is_constant() if inverse.invertible(rhs): # Apply inverse to both sides of constraint. lhs = inverse.inverse(rhs)(lhs) idx = rhs._non_const_idx()[0] expr = rhs.args[idx] if rhs.is_incr(idx): return self._canonicalize_constraint(lhs <= expr) return self._canonicalize_constraint(lhs >= expr) elif isinstance(rhs, (minimum, min_atom)): # Lower minimum. return [ c for arg in rhs.args for c in self._canonicalize_constraint(lhs <= arg) ] else: # Replace quasiconcave atom with a superlevel set. canon_args, aux_args_constr = self._canon_args(rhs) superlevel_set = sets.superlevel(rhs.copy(canon_args), t=lhs) return superlevel_set + aux_args_constr
def _canonicalize_constraint(self, constr): """Recursively canonicalize a constraint. The DQCP grammar has expresions of the form INCR* QCVX DCP and DECR* QCCV DCP ie, zero or more real/scalar increasing (or decreasing) atoms, composed with a quasiconvex (or quasiconcave) atom, composed with DCP expressions. The monotone functions are inverted by applying their inverses to both sides of a constraint. The QCVX (QCCV) atom is lowered by replacing it with its sublevel (superlevel) set. The DCP expressions are canonicalized via graph implementations. """ lhs = constr.args[0] rhs = constr.args[1] if isinstance(constr, Inequality): # taking inverses can yield +/- infinity; this is handled here. lhs_val = np.array(lhs.value) rhs_val = np.array(rhs.value) if np.all(lhs_val == -np.inf) or np.all(rhs_val == np.inf): # constraint is redundant return [True] elif np.any(lhs_val == np.inf) or np.any(rhs_val == -np.inf): # constraint is infeasible return [False] if constr.is_dcp(): canon_constr, aux_constr = self.canonicalize_tree(constr) return [canon_constr] + aux_constr # canonicalize lhs <= rhs # either lhs or rhs is quasiconvex (and not convex) assert isinstance(constr, Inequality) # short-circuit zero-valued expressions to simplify inverse logic if lhs.is_zero(): return self._canonicalize_constraint(0 <= rhs) if rhs.is_zero(): return self._canonicalize_constraint(lhs <= 0) if lhs.is_quasiconvex() and not lhs.is_convex(): # quasiconvex <= constant assert rhs.is_constant(), rhs if inverse.invertible(lhs): # Apply inverse to both sides of constraint. rhs = inverse.inverse(lhs)(rhs) idx = lhs._non_const_idx()[0] expr = lhs.args[idx] if lhs.is_incr(idx): return self._canonicalize_constraint(expr <= rhs) assert lhs.is_decr(idx) return self._canonicalize_constraint(expr >= rhs) elif isinstance(lhs, (maximum, max_atom)): # Lower maximum. return [ c for arg in lhs.args for c in self._canonicalize_constraint(arg <= rhs) ] else: # Replace quasiconvex atom with a sublevel set. canon_args, aux_args_constr = self._canon_args(lhs) sublevel_set = sets.sublevel(lhs.copy(canon_args), t=rhs) return sublevel_set + aux_args_constr # constant <= quasiconcave assert rhs.is_quasiconcave() assert lhs.is_constant() if inverse.invertible(rhs): # Apply inverse to both sides of constraint. lhs = inverse.inverse(rhs)(lhs) idx = rhs._non_const_idx()[0] expr = rhs.args[idx] if rhs.is_incr(idx): return self._canonicalize_constraint(lhs <= expr) assert rhs.is_decr(idx) return self._canonicalize_constraint(lhs >= expr) elif isinstance(rhs, (minimum, min_atom)): # Lower minimum. return [ c for arg in rhs.args for c in self._canonicalize_constraint(lhs <= arg) ] else: # Replace quasiconcave atom with a superlevel set. canon_args, aux_args_constr = self._canon_args(rhs) superlevel_set = sets.superlevel(rhs.copy(canon_args), t=lhs) return superlevel_set + aux_args_constr