Source code for picos.expressions.variables

# ------------------------------------------------------------------------------
# Copyright (C) 2019 Maximilian Stahlberg
# Based on the original picos.expressions module by Guillaume Sagnol.
#
# This file is part of PICOS.
#
# PICOS is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# PICOS is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# this program.  If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------------------------------------------

"""Implements all mathematical variable types and their base class."""

from collections import namedtuple

import cvxopt

from .. import glyphs, settings
from ..apidoc import api_end, api_start
from ..caching import cached_property
from ..containers import DetailedType
from .data import cvxopt_equals, cvxopt_maxdiff, load_shape
from .exp_affine import AffineExpression, ComplexAffineExpression
from .mutable import Mutable
from .vectorizations import (ComplexVectorization, FullVectorization,
                             HermitianVectorization,
                             LowerTriangularVectorization,
                             SkewSymmetricVectorization,
                             SymmetricVectorization,
                             UpperTriangularVectorization)

_API_START = api_start(globals())
# -------------------------------


[docs]class VariableType(DetailedType): """The detailed type of a variable for predicting reformulation outcomes.""" pass
[docs]class BaseVariable(Mutable): """Primary base class for all variable types. Variables need to inherit this class with priority (first class listed) and :class:`~.exp_affine.ComplexAffineExpression` or :class:`~.exp_affine.AffineExpression` without priority. """ # TODO: Document changed variable bound behavior: Only full bounds can be # given but they may contain (-)float("inf").
[docs] def __init__(self, name, vectorization, lower=None, upper=None): """Perform basic initialization for :class:`BaseVariable` instances. :param str name: Name of the variable. A leading `"__"` denotes a private variable and is replaced by a sequence containing the variable's unique ID. :param vectorization: Vectorization format used to store the value. :type vectorization: ~picos.expressions.vectorizations.BaseVectorization :param lower: Constant lower bound on the variable. May contain ``float("-inf")`` to denote unbounded elements. :param upper: Constant upper bound on the variable. May contain ``float("inf")`` to denote unbounded elements. """ Mutable.__init__(self, name, vectorization) self._lower = None if lower is None else self._load_vectorized(lower) self._upper = None if upper is None else self._load_vectorized(upper)
[docs] def copy(self, new_name=None): """Return an independent copy of the variable.""" name = self.name if new_name is None else new_name if self._lower is not None or self._upper is not None: return self.__class__(name, self.shape, self._lower, self._upper) else: return self.__class__(name, self.shape)
VarSubtype = namedtuple("VarSubtype", ("dim", "bnd"))
[docs] @classmethod def make_var_type(cls, *args, **kwargs): """Create a detailed variable type from subtype parameters. See also :attr:`var_type`. """ return VariableType(cls, cls.VarSubtype(*args, **kwargs))
@property def var_subtype(self): """The subtype part of the detailed variable type. See also :attr:`var_type`. """ return self.VarSubtype(self.dim, self.num_bounds) @property def var_type(self): """The detailed variable type. This intentionally does not override :meth:`Expression.type <.expression.Expression.type>` so that the variable still behaves as the affine expression that it represents when prediction constraint outcomes. """ return VariableType(self.__class__, self.var_subtype)
[docs] @cached_property def long_string(self): """Long string representation for printing a :meth:`~picos.Problem`.""" lower, upper = self.bound_dicts if lower and upper: bound_str = " (clamped)" elif lower: bound_str = " (bounded below)" elif upper: bound_str = " (bounded above)" else: bound_str = "" return "{}{}".format(super(BaseVariable, self).long_string, bound_str)
[docs] @cached_property def bound_dicts(self): """Variable bounds as a pair of mappings from index to scalar bound. The indices and bound values are with respect to the internal representation of the variable, whose value can be accessed with :attr:`~.mutable.Mutable.internal_value`. Upper and lower bounds set to ``float("inf")`` and ``float("-inf")`` on variable creation, respectively, are not included. """ posinf = float("+inf") neginf = float("-inf") if self._lower is None: lower = {} else: lower = {i: self._lower[i] for i in range(self.dim) if self._lower[i] != neginf} if self._upper is None: upper = {} else: upper = {i: self._upper[i] for i in range(self.dim) if self._upper[i] != posinf} return (lower, upper)
@property def num_bounds(self): """Number of scalar bounds associated with the variable.""" lower, upper = self.bound_dicts return len(lower) + len(upper)
[docs] @cached_property def bound_constraint(self): """The variable bounds as a PICOS constraint, or :obj:`None`.""" lower, upper = self.bound_dicts I, J, V, b = [], [], [], [] for i, bound in upper.items(): I.append(i) J.append(i) V.append(1.0) b.append(bound) offset = len(I) for i, bound in lower.items(): I.append(offset + i) J.append(i) V.append(-1.0) b.append(-bound) if not I: return None A = cvxopt.spmatrix(V, I, J, size=(len(I), self.dim), tc="d") Ax = AffineExpression(string=glyphs.Fn("bnd_con_lhs")(self.name), shape=len(I), coefficients={self: A}) return Ax <= b
[docs]class RealVariable(BaseVariable, AffineExpression): """A real-valued variable."""
[docs] def __init__(self, name, shape=(1, 1), lower=None, upper=None): """Create a :class:`RealVariable`. :param str name: The variable's name, used for both string description and identification. :param shape: The shape of a vector or matrix variable. :type shape: int or tuple or list :param lower: Constant lower bound on the variable. May contain ``float("-inf")`` to denote unbounded elements. :param upper: Constant upper bound on the variable. May contain ``float("inf")`` to denote unbounded elements. """ shape = load_shape(shape) vec = FullVectorization(shape) BaseVariable.__init__(self, name, vec, lower, upper) AffineExpression.__init__(self, self.name, shape, {self: vec.identity})
@classmethod def _get_type_string_base(cls): return "Real Variable"
[docs]class IntegerVariable(BaseVariable, AffineExpression): """An integer-valued variable."""
[docs] def __init__(self, name, shape=(1, 1), lower=None, upper=None): """Create an :class:`IntegerVariable`. :param str name: The variable's name, used for both string description and identification. :param shape: The shape of a vector or matrix variable. :type shape: int or tuple or list :param lower: Constant lower bound on the variable. May contain ``float("-inf")`` to denote unbounded elements. :param upper: Constant upper bound on the variable. May contain ``float("inf")`` to denote unbounded elements. """ shape = load_shape(shape) vec = FullVectorization(shape) BaseVariable.__init__(self, name, vec, lower, upper) AffineExpression.__init__(self, self.name, shape, {self: vec.identity})
@classmethod def _get_type_string_base(cls): return "Integer Variable" def _check_internal_value(self, value): fltData = list(value) if not fltData: return # All elements are exactly zero. intData = cvxopt.matrix([round(x) for x in fltData]) fltData = cvxopt.matrix(fltData) if not cvxopt_equals(intData, fltData, absTol=settings.ABSOLUTE_INTEGRALITY_TOLERANCE): raise ValueError("Data is not near-integral with absolute tolerance" " {:.1e}: Largest difference is {:.1e}.".format( settings.ABSOLUTE_INTEGRALITY_TOLERANCE, cvxopt_maxdiff(intData, fltData)))
[docs]class BinaryVariable(BaseVariable, AffineExpression): r"""A :math:`\{0,1\}`-valued variable."""
[docs] def __init__(self, name, shape=(1, 1)): """Create a :class:`BinaryVariable`. :param str name: The variable's name, used for both string description and identification. :param shape: The shape of a vector or matrix variable. :type shape: int or tuple or list """ shape = load_shape(shape) vec = FullVectorization(shape) BaseVariable.__init__(self, name, vec) AffineExpression.__init__(self, self.name, shape, {self: vec.identity})
@classmethod def _get_type_string_base(cls): return "Binary Variable" def _check_internal_value(self, value): fltData = list(value) if not fltData: return # All elements are exactly zero. binData = cvxopt.matrix([float(bool(round(x))) for x in fltData]) fltData = cvxopt.matrix(fltData) if not cvxopt_equals(binData, fltData, absTol=settings.ABSOLUTE_INTEGRALITY_TOLERANCE): raise ValueError("Data is not near-binary with absolute tolerance" " {:.1e}: Largest difference is {:.1e}.".format( settings.ABSOLUTE_INTEGRALITY_TOLERANCE, cvxopt_maxdiff(binData, fltData)))
[docs]class ComplexVariable(BaseVariable, ComplexAffineExpression): """A complex-valued variable. Passed to solvers as a real variable vector with :math:`2mn` entries. """
[docs] def __init__(self, name, shape=(1, 1)): """Create a :class:`ComplexVariable`. :param str name: The variable's name, used for both string description and identification. :param shape: The shape of a vector or matrix variable. :type shape: int or tuple or list """ shape = load_shape(shape) vec = ComplexVectorization(shape) BaseVariable.__init__(self, name, vec) ComplexAffineExpression.__init__( self, self.name, shape, {self: vec.identity})
@classmethod def _get_type_string_base(cls): return "Complex Variable"
[docs]class SymmetricVariable(BaseVariable, AffineExpression): r"""A symmetric matrix variable. Stored internally and passed to solvers as a symmetric vectorization with only :math:`\frac{n(n+1)}{2}` entries. """
[docs] def __init__(self, name, shape=(1, 1), lower=None, upper=None): """Create a :class:`SymmetricVariable`. :param str name: The variable's name, used for both string description and identification. :param shape: The shape of the matrix. :type shape: int or tuple or list :param lower: Constant lower bound on the variable. May contain ``float("-inf")`` to denote unbounded elements. :param upper: Constant upper bound on the variable. May contain ``float("inf")`` to denote unbounded elements. """ shape = load_shape(shape, squareMatrix=True) vec = SymmetricVectorization(shape) BaseVariable.__init__(self, name, vec, lower, upper) AffineExpression.__init__(self, self.name, shape, {self: vec.identity})
@classmethod def _get_type_string_base(cls): return "Symmetric Variable"
[docs]class SkewSymmetricVariable(BaseVariable, AffineExpression): r"""A skew-symmetric matrix variable. Stored internally and passed to solvers as a skew-symmetric vectorization with only :math:`\frac{n(n-1)}{2}` entries. """
[docs] def __init__(self, name, shape=(1, 1), lower=None, upper=None): """Create a :class:`SkewSymmetricVariable`. :param str name: The variable's name, used for both string description and identification. :param shape: The shape of the matrix. :type shape: int or tuple or list :param lower: Constant lower bound on the variable. May contain ``float("-inf")`` to denote unbounded elements. :param upper: Constant upper bound on the variable. May contain ``float("inf")`` to denote unbounded elements. """ shape = load_shape(shape, squareMatrix=True) vec = SkewSymmetricVectorization(shape) BaseVariable.__init__(self, name, vec, lower, upper) AffineExpression.__init__(self, self.name, shape, {self: vec.identity})
@classmethod def _get_type_string_base(cls): return "Skew-symmetric Variable"
[docs]class HermitianVariable(BaseVariable, ComplexAffineExpression): r"""A hermitian matrix variable. Stored internally and passed to solvers as the horizontal concatenation of a real symmetric vectorization with :math:`\frac{n(n+1)}{2}` entries and a real skew-symmetric vectorization with :math:`\frac{n(n-1)}{2}` entries, resulting in a real vector with only :math:`n^2` entries total. """
[docs] def __init__(self, name, shape): """Create a :class:`HermitianVariable`. :param str name: The variable's name, used for both string description and identification. :param shape: The shape of the matrix. :type shape: int or tuple or list """ shape = load_shape(shape, squareMatrix=True) vec = HermitianVectorization(shape) BaseVariable.__init__(self, name, vec) ComplexAffineExpression.__init__( self, self.name, shape, {self: vec.identity})
@classmethod def _get_type_string_base(cls): return "Hermitian Variable"
[docs]class LowerTriangularVariable(BaseVariable, AffineExpression): r"""A lower triangular matrix variable. Stored internally and passed to solvers as a lower triangular vectorization with only :math:`\frac{n(n+1)}{2}` entries. """
[docs] def __init__(self, name, shape=(1, 1), lower=None, upper=None): """Create a :class:`LowerTriangularVariable`. :param str name: The variable's name, used for both string description and identification. :param shape: The shape of the matrix. :type shape: int or tuple or list :param lower: Constant lower bound on the variable. May contain ``float("-inf")`` to denote unbounded elements. :param upper: Constant upper bound on the variable. May contain ``float("inf")`` to denote unbounded elements. """ shape = load_shape(shape, squareMatrix=True) vec = LowerTriangularVectorization(shape) BaseVariable.__init__(self, name, vec, lower, upper) AffineExpression.__init__(self, self.name, shape, {self: vec.identity})
@classmethod def _get_type_string_base(cls): return "Lower Triangular Variable"
[docs]class UpperTriangularVariable(BaseVariable, AffineExpression): r"""An upper triangular matrix variable. Stored internally and passed to solvers as an upper triangular vectorization with only :math:`\frac{n(n+1)}{2}` entries. """
[docs] def __init__(self, name, shape=(1, 1), lower=None, upper=None): """Create a :class:`UpperTriangularVariable`. :param str name: The variable's name, used for both string description and identification. :param shape: The shape of the matrix. :type shape: int or tuple or list :param lower: Constant lower bound on the variable. May contain ``float("-inf")`` to denote unbounded elements. :param upper: Constant upper bound on the variable. May contain ``float("inf")`` to denote unbounded elements. """ shape = load_shape(shape, squareMatrix=True) vec = UpperTriangularVectorization(shape) BaseVariable.__init__(self, name, vec, lower, upper) AffineExpression.__init__(self, self.name, shape, {self: vec.identity})
@classmethod def _get_type_string_base(cls): return "Upper Triangular Variable"
CONTINUOUS_VARTYPES = (RealVariable, ComplexVariable, SymmetricVariable, SkewSymmetricVariable, HermitianVariable, LowerTriangularVariable, UpperTriangularVariable) # -------------------------------------- __all__ = api_end(_API_START, globals())