def __rmatmul__(self, other): """ :param other: a constant Expression or nd-array which left-multiplies "self". :return: other @ self """ if other.ndim > 2 or self.ndim > 2: # pragma: no cover msg = '\n \t Matmul implementation uses "dot", ' msg += 'which behaves differently for higher dimension arrays.\n' raise RuntimeError(msg) if isinstance(other, sp.spmatrix): other = other.toarray() (A, x, B) = self.factor() if isinstance(other, Expression): if not other.is_constant(): # pragma: no cover raise RuntimeError( 'Can only multiply by constant Expressions.') else: _, _, other = other.factor() if other.ndim == 2: other_times_A = np.tensordot(other, A, axes=1) else: other_times_A = np.tensordot(other.reshape((1, -1)), A, axes=1) other_times_A = np.squeeze(other_times_A, axis=0) other_times_A_x = Expression._disjoint_dot(other_times_A, x) res = other_times_A_x other_times_B = np.tensordot(other, B, axes=1) for tup in array_index_iterator(other_times_A_x.shape): res[tup].offset = other_times_B[tup] return res
def factor(self): """ Returns a tuple ``(A, x, B)``. ``A`` is a tensor of one order higher than the current Expression object, i.e. ``A.ndim == self.ndim + 1``. The dimensions of ``A`` and ``self`` agree up until ``self.ndim``, i.e. ``A.shape[:-1] == self.shape``. ``x`` is a list of ScalarAtom objects, with ``len(x) == A.shape[-1]``. ``B`` is a numpy array of the same shape as ``self``. The purpose of this function is to enable faster matrix multiplications of Expression objects. The idea is that if you tensor-contract ``A`` along its final dimension according to ``x``, and then add ``B``, you recover this Expression. """ x = list(set(a for se in self.flat for a in se.atoms_to_coeffs)) x.sort(key=lambda a: a.id) # Sorting by ScalarAtom id makes this method deterministic # when all ScalarAtoms in this Expression are of the same type. # That's useful for, e.g. affine Expressions, which # we need to test for symbolic equivalence. atoms_to_pos = {a: i for (i, a) in enumerate(x)} A = np.zeros(self.shape + (len(x), )) B = np.zeros(self.shape) for tup in array_index_iterator(self.shape): se = self[tup] for a, c in se.atoms_to_coeffs.items(): A[tup + (atoms_to_pos[a], )] = c B[tup] = se.offset return A, x, B
def value(self, value): if isinstance(value, __REAL_TYPES__): value = np.array(value) if value.shape != self.shape: # pragma: no cover raise RuntimeError('Dimension mismatch.') for tup in array_index_iterator(self.shape): sv = list(self[tup].atoms_to_coeffs)[0] sv._value = value[tup] pass
def value(self): """ An ndarray containing numeric entries, of shape equal to ``self.shape``. This is the result of propagating the value of ScalarVariable objects through the symbolic operations tracked by this Expression. """ val = np.zeros(shape=self.shape) for tup in array_index_iterator(self.shape): val[tup] = self[tup].value return val
def is_concave(self): """ Return an ndarray of booleans. For a fixed component index, the value of the returned array indicates if that component of the current Expression is a concave function of Variables within its scope. """ res = np.empty(shape=self.shape, dtype=bool) for tup in array_index_iterator(self.shape): res[tup] = self[tup].is_concave() return res
def _disjoint_dot(array, list_of_atoms): # This is still MUCH SLOWER than adding numbers together. if len(list_of_atoms) != array.shape[-1]: # pragma: no cover raise RuntimeError('Incompatible dimensions to disjoint_dot.') expr = np.empty(shape=array.shape[:-1], dtype=object) for tup in array_index_iterator(expr.shape): dict_items = [] for i, a in enumerate(list_of_atoms): dict_items.append((a, array[tup + (i, )])) d = dict(dict_items) expr[tup] = ScalarExpression(d, 0, verify=False) return expr.view(Expression)
def scalar_variable_ids(self): """ Each component of this Variable object (i.e. each "scalar variable") contains an index which uniquely identifies it in all models where this Variable appears. Return the list of these indices. """ if self.is_proper(): return self._scalar_variable_ids else: return [ self[tup].scalar_variables()[0].id for tup in array_index_iterator(self.shape) ]
def __unstructured_populate__(obj): if obj.shape == (): v = ScalarVariable(parent=obj, index=tuple()) np.ndarray.__setitem__(obj, tuple(), ScalarExpression({v: 1}, 0, verify=False)) obj._scalar_variable_ids.append(v.id) else: for tup in array_index_iterator(obj.shape): v = ScalarVariable(parent=obj, index=tup) obj._scalar_variable_ids.append(v.id) np.ndarray.__setitem__( obj, tup, ScalarExpression({v: 1}, 0, verify=False)) pass
def abs(x, eval_only=False): """ Return a coniclifts Expression representing |x| componentwise. :param x: a coniclifts Expression. :param eval_only: bool. True if the returned Expression will not be used in an optimization problem. """ if not isinstance(x, Expression): x = Expression(x) expr = np.empty(shape=x.shape, dtype=object) for tup in array_index_iterator(expr.shape): expr[tup] = ScalarExpression({Abs(x[tup], eval_only): 1}, 0, verify=False) return expr.view(Expression)
def relent(x, y, elementwise=False): if not isinstance(x, Expression): x = Expression(x) if not isinstance(y, Expression): y = Expression(y) if x.size != y.size: raise RuntimeError('Incompatible arguments to relent.') if elementwise: expr = np.empty(shape=x.shape, dtype=object) for tup in array_index_iterator(expr.shape): expr[tup] = ScalarExpression({RelEnt(x[tup], y[tup]): 1}, 0) return expr.view(Expression) else: x = x.ravel() y = y.ravel() d = dict((RelEnt(x[i], y[i]), 1) for i in range(x.size)) return ScalarExpression(d, 0).as_expr()
def __unstructured_populate__(obj): if obj.shape == (): v = ScalarVariable(parent=obj, index=tuple()) d = defaultdict(int) d[v] = 1 se = ScalarExpression(d, 0, verify=False, copy=False) np.ndarray.__setitem__(obj, tuple(), se) obj._scalar_variable_ids.append(v.id) else: for tup in array_index_iterator(obj.shape): v = ScalarVariable(parent=obj, index=tup) obj._scalar_variable_ids.append(v.id) d = defaultdict(int) d[v] = 1 se = ScalarExpression(d, 0, verify=False, copy=False) np.ndarray.__setitem__(obj, tup, se) pass
def __symmetric_populate__(obj): if obj.ndim != 2 or obj.shape[0] != obj.shape[1]: # pragma: no cover raise RuntimeError('Symmetric variables must be 2d, and square.') temp_id_array = np.zeros(shape=obj.shape, dtype=int) for i in range(obj.shape[0]): v = ScalarVariable(parent=obj, index=(i, i)) np.ndarray.__setitem__(obj, (i, i), ScalarExpression({v: 1}, 0, verify=False)) temp_id_array[i, i] = v.id for j in range(i + 1, obj.shape[1]): v = ScalarVariable(parent=obj, index=(i, j)) np.ndarray.__setitem__( obj, (i, j), ScalarExpression({v: 1}, 0, verify=False)) np.ndarray.__setitem__( obj, (j, i), ScalarExpression({v: 1}, 0, verify=False)) temp_id_array[i, j] = v.id temp_id_array[j, i] = v.id for tup in array_index_iterator(obj.shape): obj._scalar_variable_ids.append(temp_id_array[tup]) pass
def make_variable_map(variables, var_indices): """ :param variables: a list of Variable objects with is_proper=True. These variables appear in some vectorized conic system represented by {x : A @ x + b \in K}. :param var_indices: a list of 1darrays. The i^th 1darray in this list contains the locations of the i^th Variable's entries with respect to the vectorized conic system {x : A @ x + b \in K}. :return: a dictionary mapping Variable names to ndarrays of indices. These ndarrays of indices allow the user to access a Variable's value from the vectorized conic system in a very convenient way. For example, if "my_var" is the name of a Variable with shape (10, 2, 1, 4), and "x" is feasible for the conic system {x : A @ x + b \in K}, then a feasible value for "my_var" is the 10-by-2-by-1-by-4 array given by x[variable_map['my_var']]. NOTES: This function assumes that every entry of a Variable object is an unadorned ScalarVariable. As a result, skew-symmetric Variables or Variables with sparsity patterns are not supported by this very important function. Symmetric matrices are supported by this function. If some but not-all components of a Variable "my_var" participate in a vectorized conic system {x : A @ x + b \in K }, then var_indices is presumed to map these ScalarVariables to the number -1. The number -1 will then appear as a value in variable_map. When values are set for ScalarVariable objects, the system {x : A @ x + b \in K} is extended to { [x,0] : A @ x + b \in K}. In this way, ScalarVariables which do not participate in a given optimization problem are assigned the value 0. """ variable_map = dict() for i, v in enumerate(variables): temp = np.zeros(v.shape) j = 0 for tup in util.array_index_iterator(v.shape): temp[tup] = var_indices[i][j] j += 1 variable_map[v.name] = np.array(temp, dtype=int) return variable_map
def __new__(cls, obj): attempt = np.array(obj, dtype=object, copy=False, subok=True) for tup in array_index_iterator(attempt.shape): # noinspection PyTypeChecker,PyCallByClass Expression.__setitem__(attempt, tup, attempt[tup]) return attempt.view(Expression)