def simplify(self): """ Simplify into standard form: ((const) * (params))*(var) """ c, p, v = self.symbol_groups() coeffs = c[0] if len(c) > 0 else sym.Constant(1) vars = v[0] if len(v) > 0 else None for const in c[1:]: coeffs = smul(coeffs, const, simple = True) for param in p: coeffs = smul(coeffs, param, simple = True) for variable in v[1:]: vars = smul(vars, variable, simple = True) if vars is not None: self.args[0] = coeffs self.args[1] = vars else: # Adopt inner arguments if no variables if hasattr(coeffs, 'args'): self.args[0] = coeffs.args[0] self.args[1] = coeffs.args[1] else: self.args[0] = coeffs self.args[1] = sym.Constant(1) return self # no guarantees that this return will have an audience
def __init__(self, epigraph_axes, dimensions): """ Mathematical Background: Let Q[p] = {(t,x) in R x R[p-1] | norm2(x) <= t} This thing, Q is known as a "second order cone" of dimension p. We see that t is a scalar which bounds the 2 norm of x How does the solver know how to embed t and x in the same cone? What does that mean mathematically? We can see from the above statements that we should constrain: t >= 0 x_i <= 0 for x_i in range(x) """ self.dims = dimensions # First constraint always >= 0, because cone of dimension 1 is cone R+, # the cone of non-negative reals self.constraints = [epigraph_axes[0] >= sym.Constant(0)] # Rest of constraints always <= 0 self.constraints += [ax <= sym.Constant(0) for ax in epigraph_axes[1:]]
def mul(a, b): """ Select what "kind" of multiplication to do, based on shape! """ if type(a) in [float, int]: a = sym.Constant(a) if type(b) in [float, int]: b = sym.Constant(b) if a.shape == scalar_shape or b.shape == scalar_shape: return muls.smul(a, b) else: return matrix.matmul(a, b)
def curvature(self): """ Get the curvature of the expression, disregarding parameter sign """ const,_,var = self.symbol_groups() if var == []: return 0 # AFFINE if const != []: const = const[0] else: const = sym.Constant(1) # only used for curvature analysis v = var[0] # we know that vars is at most of length 1 # because var * var is not valid DCP! if const.value > 0: return v.curvature # POSITIVE * (CONVEX/CONCAVE) elif const.value < 0: if v.curvature > 0: # NEGATIVE * CONVEX return -1 elif v.curvature < 0: # NEGATIVE * CONCAVE return +1 return 0
def simplify(self): """ Simplify the sum by simplifying the insides and flattening """ # Ensure all constants are Constant symbols to_remove = [] for n, arg in enumerate(self.args): if type(arg) in [float, int] and arg != 0: self.args[n] = sym.Constant(arg) elif type(arg) is sym.Constant and arg.value == 0: to_remove.append(n) for rm in to_remove: self.args.pop(rm) # Inner simplify for n, arg in enumerate(self.args): self.args[n] = arg.simplify() # Flatten into this sum new_args = [] for arg in self.args: if type(arg) is sum: new_args += arg.args else: new_args += [arg] self.args = new_args self.take_stock() return self # no guaranteed audience for this
def factor_coefficients(self): """ Factoring for variables. At this point there are no sub-sums Cache the factors for later retrieval. Do not replace args. This is because replacing args would require smul to handle variable distribution differently than other distribution. In other words, smul's standard form of ((const) * (params))*(var) cannot capture factorization for variables. """ factors = {n: [] for n, var in enumerate(self.vars) if var is not None} for n, var in enumerate(self.vars): #print('>>>',var) for arg in self.args: #print('...',arg) if type(arg) == ops.atoms.muls.smul: coeff = arg.coefficient_of(var) if coeff.value != 0: #print(' -->',coeff) factors[n].append(coeff) elif arg is var: #print(' -->',1) factors[n].append(sym.Constant(1)) return factors
def coefficient_of(self, var): """ Get the coefficient of this variable. Assumes standard form. """ """ If you're expecting a factored coefficient, you're doing it wrong! Instead, use the factor_coefficients() method of sums.sum """ if var is self.args[1]: return self.args[0] else: return sym.Constant(0)
def __init__(self, a, b, simple = False): self.args = [a, b] # Determine and cache the shape self.shape = util.determine_shape(self.args) # maintains parity with sum when getting offsets self.offset = sym.Constant(0) if not simple: self.simplify()
def offsets(self): offsum = [] for n, var in enumerate(self.vars): if var is None: offsum.append(self.args[n]) if offsum != []: return offsum else: return sym.Constant(0)
def coefficient_of(self, var, failraise=True): """ Find all the coefficients of a specific variable. """ self.take_stock_factors() var_index = self.find_var(var) if var_index is not None: factors = self.factor[var_index] return sum(*factors) else: return sym.Constant(0)
def __init__(self, *args, simple=False): self.args = list(args) for n, arg in enumerate(self.args): if type(arg) in [int, float]: self.args[n] = sym.Constant(arg) # Determine and cache the shape self.shape = util.determine_shape(self.args) if not simple: self.simplify()
def __new__(cls, *args, simple = False): """ Base level simplifications: 0 * x => 0 1 * x => x """ # 0 * Something if any([arg.value == 0 for arg in args]): return sym.Constant(0) # 1 * Something elif any([arg.value == 1 for arg in args]): for n, arg in enumerate(args): if arg.value == 1: return args[not n] # Constant * Constant elif all([type(arg) is sym.Constant for arg in args]): return sym.Constant(args[0].value * args[1].value) # Something * (Sum of Things) --> Something*Thing0 + Something*Thing1... elif any([type(arg) is ops.atoms.sums.sum for arg in args]): if type(args[0]) is ops.atoms.sums.sum: return distribute(args[1], args[0]) elif type(args[1]) is ops.atoms.sums.sum: return distribute(args[0], args[1]) elif any([type(arg) is sym.Vector for arg in args]): if type(args[0]) is sym.Vector: return distribute_vector(args[1], args[0]) elif type(args[1]) is sym.Vector: return distribute_vector(args[0], args[1]) else: return super().__new__(cls)