Coverage for picos/valuable.py: 72.79%
147 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-03-26 07:46 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-03-26 07:46 +0000
1# ------------------------------------------------------------------------------
2# Copyright (C) 2021 Maximilian Stahlberg
3#
4# This file is part of PICOS.
5#
6# PICOS is free software: you can redistribute it and/or modify it under the
7# terms of the GNU General Public License as published by the Free Software
8# Foundation, either version 3 of the License, or (at your option) any later
9# version.
10#
11# PICOS is distributed in the hope that it will be useful, but WITHOUT ANY
12# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
13# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License along with
16# this program. If not, see <http://www.gnu.org/licenses/>.
17# ------------------------------------------------------------------------------
19"""Common interface for objects that can have a numeric value."""
21from abc import ABC, abstractmethod
23import cvxopt
24import numpy
26from .apidoc import api_end, api_start
28_API_START = api_start(globals())
29# -------------------------------
32class NotValued(RuntimeError):
33 """The operation cannot be performed due to a mutable without a value.
35 Note that the :attr:`~Valuable.value`, :attr:`~Valuable.value_as_matrix`,
36 :attr:`~Valuable.np`, and :attr:`~Valuable.np2d` attributes do not raise
37 this exception, but return :obj:`None` instead.
38 """
40 pass
43class Valuable(ABC):
44 """Abstract base class for objects that can have a numeric value.
46 This is used by all algebraic expressions through their
47 :class:`~picos.expressions.expression.Expression` base class as well as by
48 :class:`~picos.modeling.objective.Objective` and, referencing the latter, by
49 :class:`~picos.modeling.problem.Problem` instances.
50 """
52 # --------------------------------------------------------------------------
53 # Abstract and default-implementation methods.
54 # --------------------------------------------------------------------------
56 @abstractmethod
57 def _get_valuable_string(self):
58 """Return a short string defining the valuable object."""
59 pass
61 @abstractmethod
62 def _get_value(self):
63 """Return the numeric value of the object as a CVXOPT matrix.
65 :raises NotValued: When the value is not fully defined.
67 Method implementations need to return an independent copy of the value
68 that the user is allowed to change without affecting the object.
69 """
70 pass
72 def _set_value(self, value):
73 raise NotImplementedError("Setting the value on an instance of {} is "
74 "not supported, but you can value any mutables involved instead."
75 .format(type(self).__name__))
77 # --------------------------------------------------------------------------
78 # Provided interface.
79 # --------------------------------------------------------------------------
81 def _wrap_get_value(self, asMatrix, staySafe):
82 """Enhance the implementation of :attr:`_get_value`.
84 Checks the type of any value returned and offers conversion options.
86 :param bool asMatrix:
87 Whether scalar values are returned as matrices.
89 :param bool staySafe:
90 Whether :exc:`NotValued` exceptions are raised. Otherwise missing
91 values are returned as :obj:`None`.
92 """
93 try:
94 value = self._get_value()
95 except NotValued:
96 if staySafe:
97 raise
98 else:
99 return None
101 assert isinstance(value, (cvxopt.matrix, cvxopt.spmatrix)), \
102 "Expression._get_value implementations must return a CVXOPT matrix."
104 if value.size == (1, 1) and not asMatrix:
105 return value[0]
106 else:
107 return value
109 value = property(
110 lambda self: self._wrap_get_value(asMatrix=False, staySafe=False),
111 lambda self, x: self._set_value(x),
112 lambda self: self._set_value(None),
113 r"""Value of the object, or :obj:`None`.
115 For an expression, it is defined if the expression is constant or if all
116 mutables involved in the expression are valued. Mutables can be valued
117 directly by writing to their :attr:`value` attribute. Variables are also
118 valued by PICOS when an optimization solution is found.
120 Some expressions can also be valued directly if PICOS can find a minimal
121 norm mutable assignment that makes the expression have the desired
122 value. In particular, this works with affine expressions whose linear
123 part has an under- or well-determined coefficient matrix.
125 If you prefer the value as a NumPy, use :attr:`np` instead.
127 :returns:
128 The value as a Python scalar or CVXOPT matrix, or :obj:`None` if it
129 is not defined.
131 :Distinction:
133 - Unlike :attr:`safe_value` and :attr:`safe_value_as_matrix`, an
134 undefined value is returned as :obj:`None`.
135 - Unlike :attr:`value_as_matrix` and :attr:`safe_value_as_matrix`,
136 scalars are returned as scalar types.
137 - For uncertain expressions, see also
138 :meth:`~.uexpression.UncertainExpression.worst_case_value`.
140 :Example:
142 >>> from picos import RealVariable
143 >>> x = RealVariable("x", (1,3))
144 >>> y = RealVariable("y", (1,3))
145 >>> e = x - 2*y + 3
146 >>> print("e:", e)
147 e: x - 2·y + [3]
148 >>> e.value = [4, 5, 6]
149 >>> print("e: ", e, "\nx: ", x, "\ny: ", y, sep = "")
150 e: [ 4.00e+00 5.00e+00 6.00e+00]
151 x: [ 2.00e-01 4.00e-01 6.00e-01]
152 y: [-4.00e-01 -8.00e-01 -1.20e+00]
153 """)
155 safe_value = property(
156 lambda self: self._wrap_get_value(asMatrix=False, staySafe=True),
157 lambda self, x: self._set_value(x),
158 lambda self: self._set_value(None),
159 """Value of the object, if defined.
161 Refer to :attr:`value` for when it is defined.
163 :returns:
164 The value as a Python scalar or CVXOPT matrix.
166 :raises ~picos.NotValued:
167 If the value is not defined.
169 :Distinction:
171 - Unlike :attr:`value`, an undefined value raises an exception.
172 - Like :attr:`value`, scalars are returned as scalar types.
173 """)
175 value_as_matrix = property(
176 lambda self: self._wrap_get_value(asMatrix=True, staySafe=False),
177 lambda self, x: self._set_value(x),
178 lambda self: self._set_value(None),
179 r"""Value of the object as a CVXOPT matrix type, or :obj:`None`.
181 Refer to :attr:`value` for when it is defined (not :obj:`None`).
183 :returns:
184 The value as a CVXOPT matrix, or :obj:`None` if it is not defined.
186 :Distinction:
188 - Like :attr:`value`, an undefined value is returned as :obj:`None`.
189 - Unlike :attr:`value`, scalars are returned as :math:`1 \times 1`
190 matrices.
191 """)
193 safe_value_as_matrix = property(
194 lambda self: self._wrap_get_value(asMatrix=True, staySafe=True),
195 lambda self, x: self._set_value(x),
196 lambda self: self._set_value(None),
197 r"""Value of the object as a CVXOPT matrix type, if defined.
199 Refer to :attr:`value` for when it is defined.
201 :returns:
202 The value as a CVXOPT matrix.
204 :raises ~picos.NotValued:
205 If the value is not defined.
207 :Distinction:
209 - Unlike :attr:`value`, an undefined value raises an exception.
210 - Unlike :attr:`value`, scalars are returned as :math:`1 \times 1`
211 matrices.
212 """)
214 @property
215 def np2d(self):
216 """Value of the object as a 2D NumPy array, or :obj:`None`.
218 Refer to :attr:`value` for when it is defined (not :obj:`None`).
220 :returns:
221 The value as a two-dimensional :class:`numpy.ndarray`, or
222 :obj:`None`, if the value is not defined.
224 :Distinction:
226 - Like :attr:`np`, values are returned as NumPy types or :obj:`None`.
227 - Unlike :attr:`np`, both scalar and vectorial values are returned as
228 two-dimensional arrays. In particular, row and column vectors are
229 distinguished.
230 """
231 value = self.value_as_matrix
233 if value is None:
234 return None
236 # Convert CVXOPT sparse to CVXOPT dense.
237 if isinstance(value, cvxopt.spmatrix):
238 value = cvxopt.matrix(value)
240 assert isinstance(value, cvxopt.matrix)
242 # Convert CVXOPT dense to a NumPy 2D array.
243 value = numpy.array(value)
245 assert len(value.shape) == 2
247 return value
249 @np2d.setter
250 def np2d(self, value):
251 self._set_value(value)
253 @np2d.deleter
254 def np2d(self):
255 self._set_value(None)
257 @property
258 def np(self):
259 """Value of the object as a NumPy type, or :obj:`None`.
261 Refer to :attr:`value` for when it is defined (not :obj:`None`).
263 :returns:
264 A one- or two-dimensional :class:`numpy.ndarray`, if the value is a
265 vector or a matrix, respectively, or a NumPy scalar type such as
266 :obj:`numpy.float64`, if the value is a scalar, or :obj:`None`,
267 if the value is not defined.
269 :Distinction:
271 - Like :attr:`value` and :attr:`np2d`, an undefined value is returned as
272 :obj:`None`.
273 - Unlike :attr:`value`, scalars are returned as NumPy scalar types as
274 opposed to Python builtin scalar types while vectors and matrices are
275 returned as NumPy arrays as opposed to CVXOPT matrices.
276 - Unlike :attr:`np2d`, scalars are returned as NumPy scalar types and
277 vectors are returned as NumPy one-dimensional arrays as opposed to
278 always returning two-dimensional arrays.
280 :Example:
282 >>> from picos import ComplexVariable
283 >>> Z = ComplexVariable("Z", (3, 3))
284 >>> Z.value = [i + i*1j for i in range(9)]
286 Proper matrices are return as 2D arrays:
288 >>> Z.value # CVXOPT matrix.
289 <3x3 matrix, tc='z'>
290 >>> Z.np # NumPy 2D array.
291 array([[0.+0.j, 3.+3.j, 6.+6.j],
292 [1.+1.j, 4.+4.j, 7.+7.j],
293 [2.+2.j, 5.+5.j, 8.+8.j]])
295 Both row and column vectors are returned as 1D arrays:
297 >>> z = Z[:,0] # First column of Z.
298 >>> z.value.size # CVXOPT column vector.
299 (3, 1)
300 >>> z.T.value.size # CVXOPT row vector.
301 (1, 3)
302 >>> z.value == z.T.value
303 False
304 >>> z.np.shape # NumPy 1D array.
305 (3,)
306 >>> z.T.np.shape # Same array.
307 (3,)
308 >>> from numpy import array_equal
309 >>> array_equal(z.np, z.T.np)
310 True
312 Scalars are returned as NumPy types:
314 >>> u = Z[0,0] # First element of Z.
315 >>> type(u.value) # Python scalar.
316 <class 'complex'>
317 >>> type(u.np) # NumPy scalar. #doctest: +SKIP
318 <class 'numpy.complex128'>
320 Undefined values are returned as None:
322 >>> del Z.value
323 >>> Z.value is Z.np is None
324 True
325 """
326 value = self.np2d
328 if value is None:
329 return None
330 elif value.shape == (1, 1):
331 return value[0, 0]
332 elif 1 in value.shape:
333 return numpy.ravel(value)
334 else:
335 return value
337 @np.setter
338 def np(self, value):
339 self._set_value(value)
341 @np.deleter
342 def np(self):
343 self._set_value(None)
345 @property
346 def sp(self):
347 """Value as a ScipPy sparse matrix or a NumPy 2D array or :obj:`None`.
349 If PICOS stores the value internally as a CVXOPT sparse matrix, or
350 equivalently if :attr:`value_as_matrix` returns an instance of
351 :func:`cvxopt.spmatrix`, then this returns the value as a :class:`SciPy
352 sparse matrix in CSC format <scipy.sparse.csc_matrix>`. Otherwise, this
353 property is equivalent to :attr:`np2d` and returns a two-dimensional
354 NumPy array, or :obj:`None`, if the value is undefined.
356 :Example:
358 >>> import picos, cvxopt
359 >>> X = picos.RealVariable("X", (3, 3))
360 >>> X.value = cvxopt.spdiag([1, 2, 3]) # Stored as a sparse matrix.
361 >>> type(X.value)
362 <class 'cvxopt.base.spmatrix'>
363 >>> type(X.sp)
364 <class 'scipy.sparse._csc.csc_matrix'>
365 >>> X.value = range(9) # Stored as a dense matrix.
366 >>> type(X.value)
367 <class 'cvxopt.base.matrix'>
368 >>> type(X.sp)
369 <class 'numpy.ndarray'>
370 """
371 import scipy.sparse
373 value = self.value_as_matrix
375 if value is None:
376 return None
377 elif isinstance(value, cvxopt.spmatrix):
378 return scipy.sparse.csc_matrix(
379 tuple(list(x) for x in reversed(value.CCS)), value.size)
380 else:
381 return numpy.array(value)
383 @property
384 def valued(self):
385 """Whether the object is valued.
387 .. note::
389 Querying this attribute is *not* faster than immediately querying
390 :attr:`value` and checking whether it is :obj:`None`. Use it only if
391 you do not need to know the value, but only whether it is available.
393 :Example:
395 >>> from picos import RealVariable
396 >>> x = RealVariable("x", 3)
397 >>> x.valued
398 False
399 >>> x.value
400 >>> print((x|1))
401 ∑(x)
402 >>> x.value = [1, 2, 3]
403 >>> (x|1).valued
404 True
405 >>> print((x|1))
406 6.0
407 """
408 try:
409 self._get_value()
410 except NotValued:
411 return False
412 else:
413 return True
415 @valued.setter
416 def valued(self, x):
417 if x is False:
418 self._set_value(None)
419 else:
420 raise ValueError("You may only assign 'False' to the 'valued' "
421 "attribute, which is the same as setting 'value' to 'None'.")
423 def __index__(self):
424 """Propose the value as an index."""
425 value = self.value_as_matrix
427 if value is None:
428 raise NotValued("Cannot use unvalued {} as an index."
429 .format(self._get_valuable_string()))
431 if value.size != (1, 1):
432 raise TypeError("Cannot use multidimensional {} as an index."
433 .format(self._get_valuable_string()))
435 value = value[0]
437 if value.imag:
438 raise ValueError(
439 "Cannot use {} as an index as its value of {} has a nonzero "
440 "imaginary part.".format(self._get_valuable_string(), value))
442 value = value.real
444 if not value.is_integer():
445 raise ValueError("Cannot use {} as an index as its value of {} is "
446 "not integral.".format(self._get_valuable_string(), value))
448 return int(value)
450 def _casting_helper(self, theType):
451 assert theType in (int, float, complex)
453 value = self.value_as_matrix
455 if value is None:
456 raise NotValued("Cannot cast unvalued {} as {}."
457 .format(self._get_valuable_string(), theType.__name__))
459 if value.size != (1, 1):
460 raise TypeError(
461 "Cannot cast multidimensional {} as {}."
462 .format(self._get_valuable_string(), theType.__name__))
464 value = value[0]
466 return theType(value)
468 def __int__(self):
469 """Cast the value to an :class:`int`."""
470 return self._casting_helper(int)
472 def __float__(self):
473 """Cast the value to a :class:`float`."""
474 return self._casting_helper(float)
476 def __complex__(self):
477 """Cast the value to a :class:`complex`."""
478 return self._casting_helper(complex)
480 def __round__(self, ndigits=None):
481 """Round the value to a certain precision."""
482 return round(float(self), ndigits)
484 def __array__(self, dtype=None):
485 """Return the value as a :class:`NumPy array <numpy.ndarray>`."""
486 value = self.safe_value_as_matrix
488 # Convert CVXOPT sparse to CVXOPT dense.
489 if isinstance(value, cvxopt.spmatrix):
490 value = cvxopt.matrix(value)
492 assert isinstance(value, cvxopt.matrix)
494 # Convert CVXOPT dense to a NumPy 2D array.
495 value = numpy.array(value, dtype)
497 assert len(value.shape) == 2
499 # Remove dimensions of size one.
500 if value.shape == (1, 1):
501 return numpy.reshape(value, ())
502 elif 1 in value.shape:
503 return numpy.ravel(value)
504 else:
505 return value
507 # Prevent NumPy operators from loading PICOS expressions as arrays.
508 __array_priority__ = float("inf")
509 __array_ufunc__ = None
512def patch_scipy_array_priority():
513 """Monkey-patch scipy.sparse to make it respect ``__array_priority__``.
515 This works around https://github.com/scipy/scipy/issues/4819 and is inspired
516 by CVXPY's scipy_wrapper.py.
517 """
518 import scipy.sparse
520 def teach_array_priority(operator):
521 def respect_array_priority(self, other):
522 if hasattr(other, "__array_priority__") \
523 and self.__array_priority__ < other.__array_priority__:
524 return NotImplemented
525 else:
526 return operator(self, other)
528 return respect_array_priority
530 base_type = scipy.sparse.spmatrix
531 matrix_types = (type_ for type_ in scipy.sparse.__dict__.values()
532 if isinstance(type_, type) and issubclass(type_, base_type))
534 for matrix_type in matrix_types:
535 for operator_name in (
536 "__add__", "__div__", "__eq__", "__ge__", "__gt__", "__le__",
537 "__lt__", "__matmul__", "__mul__", "__ne__", "__pow__", "__sub__",
538 "__truediv__",
539 ):
540 operator = getattr(matrix_type, operator_name)
542 # Wrap all binary operators of the base class and all overrides.
543 if matrix_type is base_type \
544 or operator is not getattr(base_type, operator_name):
545 wrapped_operator = teach_array_priority(operator)
546 setattr(matrix_type, operator_name, wrapped_operator)
549# --------------------------------------
550__all__ = api_end(_API_START, globals())