Source code for PEPit.point

import numpy as np

from PEPit.expression import Expression

from PEPit.tools.dict_operations import merge_dict, prune_dict, multiply_dicts


[docs] class Point(object): """ A :class:`Point` encodes an element of a pre-Hilbert space, either a point or a gradient. Attributes: name (str): A name set through the set_name method. None is no name is given. _is_leaf (bool): True if self is defined from scratch (not as linear combination of other :class:`Point` objects). False if self is defined as linear combination of other points. _value (nd.array): 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:`Point` objects. Keys are :class:`Point` objects. And values are their associated coefficients. counter (int): counts the number of leaf :class:`Point` objects. :class:`Point` objects can be added or subtracted together. They can also be multiplied and divided by a scalar value. Example: >>> point1 = Point() >>> point2 = Point() >>> new_point = (- point1 + point2) / 5 As in any pre-Hilbert space, there exists a scalar product. Therefore, :class:`Point` objects can be multiplied together. Example: >>> point1 = Point() >>> point2 = Point() >>> new_expr = point1 * point2 The output is a scalar of type :class:`Expression`. The corresponding squared norm can also be computed. Example: >>> point = Point() >>> new_expr = point ** 2 """ # Class counter. # It counts the dimension of the system of points, # namely the number of points needed to linearly generate the others. counter = 0 list_of_leaf_points = list() def __init__(self, is_leaf=True, decomposition_dict=None, name=None, ): """ :class:`Point` objects can also be instantiated via the following arguments Args: is_leaf (bool): True if self is a :class:`Point` defined from scratch (not as linear combination of other :class:`Point` objects). False if self is a linear combination of existing :class:`Point` objects. decomposition_dict (dict): decomposition of self as a linear combination of **leaf** :class:`Point` objects. Keys are :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:`Point` object of the first example can be done by Example: >>> point1 = Point() >>> point2 = Point() >>> new_point = Point(is_leaf=False, decomposition_dict = {point1: -1/5, point2: 1/5}) """ # Initialize name of the point 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, the decomposition is updated w.r.t the new direction, # 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 = Point.counter Point.counter += 1 Point.list_of_leaf_points.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:`Point` objects together, leading to a new :class:`Point`. Args: other (Point): any other :class:`Point` object. Returns: self + other (Expression): The sum of the 2 :class:`Point` objects. Raises: TypeError: if provided `other` is not a :class:`Point`. """ # Verify that other is a Point assert isinstance(other, Point) # Update the linear decomposition of the sum of 2 points from their respective leaf decomposition merged_decomposition_dict = merge_dict(self.decomposition_dict, other.decomposition_dict) merged_decomposition_dict = prune_dict(merged_decomposition_dict) # Create and return the newly created Point that cannot be a leaf, by definition return Point(is_leaf=False, decomposition_dict=merged_decomposition_dict) def __sub__(self, other): """ Subtract 2 :class:`Point` objects together, leading to a new :class:`Point`. Args: other (Point): any other :class:`Point` object. Returns: self - other (Expression): The difference between the 2 :class:`Point` objects. Raises: TypeError: if provided `other` is not a :class:`Point`. """ # A-B = A+(-B) return self.__add__(-other) def __neg__(self): """ Compute the opposite of a :class:`Point`. Returns: - self (Point): the opposite of self. """ # -A = (-1)*A return self.__rmul__(other=-1) def __rmul__(self, other): """ Multiply a :class:`Point` by a scalar value or another :class:`Point`. Args: other (int or float or Point): any scalar constant or :class:`Point` object. Returns: other * self (Point or Expression): resulting product. Raises: TypeError: if provided `other` is neither a scalar value nor a :class:`Point`. """ # Multiplying by a scalar value is applying a homothety if isinstance(other, int) or isinstance(other, float): # Build the decomposition of the new point new_decomposition_dict = dict() for key, value in self.decomposition_dict.items(): new_decomposition_dict[key] = value * other # Create and return the newly created point return Point(is_leaf=False, decomposition_dict=new_decomposition_dict) # Multiplying by another point leads to an expression encoding the inner product of the 2 points. elif isinstance(other, Point): # Compute the decomposition dict of the new expression decomposition_dict = multiply_dicts(self.decomposition_dict, other.decomposition_dict) # Create and return the new expression return Expression(is_leaf=False, decomposition_dict=decomposition_dict) else: # Raise an error if the user tries to multiply a point by anything else raise TypeError("Points can be multiplied by scalar constants and other points only!" "Got {}".format(type(other))) def __mul__(self, other): """ Multiply a :class:`Point` by a scalar value or another :class:`Point`. Args: other (int or float or Point): any scalar constant or :class:`Point` object. Returns: self * other (Point or Expression): resulting product. Raises: TypeError: if provided `other` is neither a scalar value nor a :class:`Point`. """ return self.__rmul__(other=other) def __truediv__(self, denominator): """ Divide a :class:`Point` by a scalar value. Args: denominator (int or float): any scalar constant. Returns: self / other (Point): The ratio between this :class:`Point` and the scalar value `other`. Raises: TypeError: if provided `other` is not a scalar value. """ # P / v = P * (1/v) return self.__rmul__(1 / denominator) def __pow__(self, power): """ Compute the squared norm of this :class:`Point`. Returns: self ** 2 (Expression): square norm of self. Raises: AssertionError: if provided `power` is not 2. """ # Works only for power=2 assert power == 2 # Return the inner product of a point by itself return self.__rmul__(self)
[docs] def eval(self): """ Compute, store and return the value of this :class:`Point`. Returns: self._value (np.array): The value of this :class:`Point` after the corresponding PEP was solved numerically. Raises: ValueError("The PEP must be solved to evaluate Points!") if the PEP has not been solved yet. """ # If the attribute value is not None, then simply return it. # Otherwise, compute it and return it. if self._value is None: # If leaf, the PEP would have filled the attribute after solving the problem. if self._is_leaf: raise ValueError("The PEP must be solved to evaluate Points!") # If linear combination, combine the values of the leaf, and store the result before returning it. else: value = np.zeros(Point.counter) for point, weight in self.decomposition_dict.items(): value += weight * point.eval() self._value = value return self._value
# Define a null Point initialized to 0. null_point = Point(is_leaf=False, decomposition_dict=dict())