Source code for PEPit.expression

import warnings
import numpy as np

from PEPit.constraint import Constraint

from PEPit.tools.dict_operations import merge_dict, prune_dict


[docs] class Expression(object): """ An :class:`Expression` is a linear combination of functions values, inner products of points and / or gradients (product of 2 :class:`Point` objects), and constant scalar values. Attributes: name (str): A name set through the set_name method. None is no name is given. _is_leaf (bool): True if self is a function value defined from scratch (not as linear combination of other function values). False if self is a linear combination of existing :class:`Expression` objects. _value (float): numerical value of self obtained after solving the PEP via SDP solver. Set to None before the call to the method `PEP.solve` from the :class:`PEP`. decomposition_dict (dict): decomposition of self as a linear combination of **leaf** :class:`Expression` objects. Keys are :class:`Expression` objects or tuple of 2 :class:`Point` objects. And values are their associated coefficients. counter (int): counts the number of **leaf** :class:`Expression` objects. :class:`Expression` objects can be added or subtracted together. They can also be added, subtracted, multiplied and divided by a scalar value. Example: >>> expr1 = Expression() >>> expr2 = Expression() >>> new_expr = (- expr1 + expr2 - 1) / 5 :class:`Expression` objects can also be compared together Example: >>> expr1 = Expression() >>> expr2 = Expression() >>> inequality1 = expr1 <= expr2 >>> inequality2 = expr1 >= expr2 >>> equality = expr1 == expr2 The three outputs `inequality1`, `inequality2` and `equality` are then :class:`Constraint` objects. """ # Class counter. # It counts the number of function values needed to linearly generate the expressions. counter = 0 list_of_leaf_expressions = list() def __init__(self, is_leaf=True, decomposition_dict=None, name=None, ): """ :class:`Expression` objects can also be instantiated via the following arguments Args: is_leaf (bool): True if self is a function value defined from scratch (not as linear combination of other function values). False if self is a linear combination of existing :class:`Expression` objects. decomposition_dict (dict): decomposition of self as a linear combination of **leaf** :class:`Expression` objects. Keys are :class:`Expression` objects or tuple of 2 :class:`Point` objects. And values are their associated coefficients. name (str): name of the object. None by default. Can be updated later through the method `set_name`. Note: If `is_leaf` is True, then `decomposition_dict` must be provided as None. Then `self.decomposition_dict` will be set to `{self: 1}`. Instantiating the :class:`Expression` object of the first example can be done by Example: >>> expr1 = Expression() >>> expr2 = Expression() >>> new_expr = Expression(is_leaf=False, decomposition_dict = {expr1: -1/5, expr2: 1/5, 1: -1/5}) """ # Initialize name of the expression self.name = name # Store is_leaf in a protected attribute self._is_leaf = is_leaf # Initialize the value attribute to None until the PEP is solved self._value = None # If leaf function value, the decomposition is updated, # the object counter is set # and the class counter updated. # Otherwise, the decomposition_dict is stored in an attribute and the object counter is set to None if is_leaf: assert decomposition_dict is None self.decomposition_dict = {self: 1} self.counter = Expression.counter Expression.counter += 1 Expression.list_of_leaf_expressions.append(self) else: assert type(decomposition_dict) == dict self.decomposition_dict = decomposition_dict self.counter = None
[docs] def set_name(self, name): """ Assign a name to self for easier identification purpose. Args: name (str): a name to be given to self. """ self.name = name
[docs] def get_name(self): """ Returns (str): the attribute name. """ return self.name
[docs] def get_is_leaf(self): """ Returns: self._is_leaf (bool): allows to access the protected attribute `_is_leaf`. """ return self._is_leaf
def __add__(self, other): """ Add 2 :class:`Expression` objects together, leading to a new :class:`Expression` object. Note an :class:`Expression` can also be added to a python `float` or `int`. Args: other (Expression or int or float): any other :class:`Expression` object or scalar constant. Returns: self + other (Expression): The sum of the 2 :class:`Expression` objects or of an :class:`Expression` and a scalar. Raises: TypeError: if provided `other` is neither an :class:`Expression` nor a scalar value. """ # If other is an Expression, merge the decomposition_dicts if isinstance(other, Expression): merged_decomposition_dict = merge_dict(self.decomposition_dict, other.decomposition_dict) # If other is a scalar constant, add it to the decomposition_dict of self elif isinstance(other, int) or isinstance(other, float): merged_decomposition_dict = merge_dict(self.decomposition_dict, {1: other}) # Raise an Exception in any other scenario else: raise TypeError("Expression can be added only to other expression or scalar values!" "Got {}".format(type(other))) # Remove leaf with null coefficients merged_decomposition_dict = prune_dict(merged_decomposition_dict) # Create and return the newly created Expression return Expression(is_leaf=False, decomposition_dict=merged_decomposition_dict) def __radd__(self, other): """ Add 2 :class:`Expression` objects together, leading to a new :class:`Expression` object. Note an :class:`Expression` can also be added to a python `float` or `int`. Args: other (Expression or int or float): any other :class:`Expression` object or scalar constant. Returns: other + self (Expression): The sum of the 2 :class:`Expression` objects or of an :class:`Expression` and a scalar. Raises: TypeError: if provided `other` is neither an :class:`Expression` nor a scalar value. """ return self.__add__(other=other) def __sub__(self, other): """ Subtract 2 :class:`Expression` objects together, leading to a new :class:`Expression` object. Note a python `float` or `int` can also be subtracted from an :class:`Expression`. Args: other (Expression or int or float): any other :class:`Expression` object or scalar constant. Returns: self - other (Expression): the difference between the 2 :class:`Expression` objects. Raises: TypeError: if provided `other` is neither an :class:`Expression` nor a scalar value. """ # A - B = A + (-B) return self.__add__(-other) def __rsub__(self, other): """ Subtract 2 :class:`Expression` objects together, leading to a new :class:`Expression` object. Note an :class:`Expression` can also be subtracted from a python `float` or `int`. Args: other (Expression or int or float): any other :class:`Expression` object or scalar constant. Returns: other - self (Expression): the difference between the 2 :class:`Expression` objects or between a scalar and an :class:`Expression`. Raises: TypeError: if provided `other` is neither an :class:`Expression` nor a scalar value. """ # B - A = -(A - B) return - self.__sub__(other=other) def __neg__(self): """ Compute the opposite of an :class:`Expression`. Returns: - self (Expression): the opposite of self. """ # -A = (-1) * A return self.__rmul__(other=-1) def __rmul__(self, other): """ Multiply an :class:`Expression` by a scalar value. Args: other (int or float): any scalar constant Returns: other * self (Expression): the product of the 2 :class:`Expression` objects. Raises: AssertionError: if provided `other` is not a scalar value. """ # Verify other is a scalar constant assert isinstance(other, int) or isinstance(other, float) # Multiply uniformly self's decomposition_dict by other new_decomposition_dict = dict() for key, value in self.decomposition_dict.items(): new_decomposition_dict[key] = value * other # Create and return the newly created Expression return Expression(is_leaf=False, decomposition_dict=new_decomposition_dict) def __mul__(self, other): """ Multiply an :class:`Expression` by a scalar value. Args: other (int or float): any scalar constant Returns: self * other (Expression): the product of the 2 :class:`Expression` objects. Raises: AssertionError: if provided `other` is not a scalar value. """ return self.__rmul__(other=other) def __truediv__(self, denominator): """ Divide an :class:`Expression` by a scalar value. Args: denominator (int or float): the scalar value to divide by. Returns: self / other (Expression): the ratio between the 2 :class:`Expression` objects. Raises: AssertionError: if provided `other` is not a scalar value. """ # P / v = P * (1/v) return self.__rmul__(other=1 / denominator) def __le__(self, other): """ Create an inequality :class:`Constraint` object from an inequality between two :class:`Expression` objects. Args: other (Expression of int or float): any :class:`Expression` of python scalar object. Returns: self :math:`\\leq` other (Expression): :class:`Constraint` object encoding the corresponding inequality. """ return Constraint(self - other, equality_or_inequality='inequality') def __lt__(self, other): """ Create an inequality :class:`Constraint` object from an inequality between two :class:`Expression` objects. Args: other (Expression of int or float): any :class:`Expression` of python scalar object. Returns: self < other (Expression): :class:`Constraint` object encoding the corresponding inequality. Note: The input inequality is strict, but optimizing over the interior set is equivalent to considering the large one, so we refer to the latest. Raises: Warnings("Strict constraints will lead to the same solution as under soft constraints") """ warnings.warn("Strict constraints will lead to the same solution as under soft constraints") return self.__le__(other=other) def __ge__(self, other): """ Create an inequality :class:`Constraint` object from an inequality between two :class:`Expression` objects. Args: other (Expression of int or float): any :class:`Expression` of python scalar object. Returns: other :math:`\\leq` self (Expression): :class:`Constraint` object encoding the corresponding inequality. """ return -self <= -other def __gt__(self, other): """ Create an inequality :class:`Constraint` object from an inequality between two :class:`Expression` objects. Args: other (Expression of int or float): any :class:`Expression` of python scalar object. Returns: other < self (Expression): :class:`Constraint` object encoding the corresponding inequality. Note: The input inequality is strict, but optimizing over the interior set is equivalent to considering the large one, so we refer to the latest. Raises: Warnings("Strict constraints will lead to the same solution as under soft constraints") """ warnings.warn("Strict constraints will lead to the same solution as under soft constraints") return self.__ge__(other=other) def __eq__(self, other): """ Create an equality :class:`Constraint` object from an equality between two :class:`Expression` objects. Args: other (Expression of int or float): any :class:`Expression` of python scalar object. Returns: self = other (Expression): :class:`Constraint` object encoding the corresponding equality. """ return Constraint(self - other, equality_or_inequality='equality') def __hash__(self): return super().__hash__()
[docs] def eval(self): """ Compute, store and return the value of this :class:`Expression`. Returns: self._value (np.array): Value of this :class:`Expression` after the corresponding PEP was solved numerically. Raises: ValueError("The PEP must be solved to evaluate Expressions!") if the PEP has not been solved yet. TypeError("Expressions are made of function values, inner products and constants only!") """ # If the attribute value is not None, then simply return it. # Otherwise, compute it and return it. if self._value is None: # If leaf function value, the PEP would have filled the attribute after solving the problem. if self._is_leaf: raise ValueError("The PEP must be solved to evaluate Expressions!") # If linear combination, # combine the values of the leaf expressions, # and store the result before returning it. else: value = 0 for key, weight in self.decomposition_dict.items(): # Distinguish 3 cases: function values, inner products, and constant values if type(key) == Expression: assert key.get_is_leaf() value += weight * key.eval() elif type(key) == tuple: point1, point2 = key assert point1.get_is_leaf() assert point2.get_is_leaf() value += weight * np.dot(point1.eval(), point2.eval()) elif key == 1: value += weight # Raise Exception out of those 3 cases else: raise TypeError("Expressions are made of function values, inner products and constants only!" "Got {}".format(type(key))) # Store the value self._value = value # Return the value return self._value
# Define a null Expression initialized to 0. null_expression = Expression(is_leaf=False, decomposition_dict=dict())