1#  ___________________________________________________________________________
2#
3#  Pyomo: Python Optimization Modeling Objects
4#  Copyright 2017 National Technology and Engineering Solutions of Sandia, LLC
5#  Under the terms of Contract DE-NA0003525 with National Technology and
6#  Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
7#  rights in this software.
8#  This software is distributed under the 3-clause BSD License.
9#  ___________________________________________________________________________
10"""Various conic constraint implementations."""
11
12from pyomo.core.expr.numvalue import is_numeric_data
13from pyomo.core.expr.current import (value,
14                                     exp)
15from pyomo.core.kernel.block import block
16from pyomo.core.kernel.variable import (IVariable,
17                                        variable,
18                                        variable_tuple)
19from pyomo.core.kernel.constraint import (IConstraint,
20                                          linear_constraint,
21                                          constraint,
22                                          constraint_tuple)
23
24def _build_linking_constraints(v, v_aux):
25    assert len(v) == len(v_aux)
26    c_aux = []
27    for vi, vi_aux in zip(v, v_aux):
28        assert vi_aux.ctype is IVariable
29        if vi is None:
30            continue
31        elif is_numeric_data(vi):
32            c_aux.append(
33                linear_constraint(variables=(vi_aux,),
34                                  coefficients=(1,),
35                                  rhs=vi))
36        elif isinstance(vi, IVariable):
37            c_aux.append(
38                linear_constraint(variables=(vi_aux, vi),
39                                  coefficients=(1, -1),
40                                  rhs=0))
41        else:
42            c_aux.append(
43                constraint(body=vi_aux - vi,
44                           rhs=0))
45    return constraint_tuple(c_aux)
46
47class _ConicBase(IConstraint):
48    """Base class for a few conic constraints that
49    implements some shared functionality. Derived classes
50    are expected to declare any necessary slots."""
51    _ctype = IConstraint
52    _linear_canonical_form = False
53    __slots__ = ()
54
55    def __init__(self):
56        self._parent = None
57        self._storage_key = None
58        self._active = True
59        # the body expression is only built if necessary
60        # (i.e., when someone asks for it via the body
61        # property method)
62        self._body = None
63
64    @classmethod
65    def as_domain(cls, *args, **kwds):
66        """Builds a conic domain"""
67        raise NotImplementedError     #pragma:nocover
68
69    def _body_function(self, *args):
70        """A function that defines the body expression"""
71        raise NotImplementedError     #pragma:nocover
72
73    def _body_function_variables(self, values=False):
74        """Returns variables in the order they should be
75        passed to the body function. If values is True, then
76        return the current value of each variable in place
77        of the variables themselves."""
78        raise NotImplementedError     #pragma:nocover
79
80    def check_convexity_conditions(self, relax=False):
81        """Returns True if all convexity conditions for the
82        conic constraint are satisfied. If relax is True,
83        then variable domains are ignored and it is assumed
84        that all variables are continuous."""
85        raise NotImplementedError     #pragma:nocover
86
87    #
88    # Define the IConstraint abstract methods
89    #
90
91    @property
92    def body(self):
93        """The body of the constraint"""
94        if self._body is None:
95            self._body = self._body_function(
96                *self._body_function_variables(values=False))
97        return self._body
98
99    @property
100    def lb(self):
101        """The lower bound of the constraint"""
102        return None
103
104    @property
105    def ub(self):
106        """The upper bound of the constraint"""
107        return 0.0
108
109    @property
110    def rhs(self):
111        """The right-hand side of the constraint"""
112        raise ValueError(
113            "The rhs property can not be read because this "
114            "is not an equality constraint")
115
116    @property
117    def equality(self):
118        return False
119
120    #
121    # Override a the default __call__ method on IConstraint
122    # to avoid building the body expression, if possible
123    #
124
125    def __call__(self, exception=True):
126        try:
127            # we wrap the result with value(...) as the
128            # alpha term used by some of the constraints
129            # may be a parameter
130            return value(self._body_function(
131                *self._body_function_variables(values=True)))
132        except (ValueError, TypeError):
133            if exception:
134                raise ValueError("one or more terms "
135                                 "could not be evaluated")
136            return None
137
138class quadratic(_ConicBase):
139    """A quadratic conic constraint of the form:
140
141        x[0]^2 + ... + x[n-1]^2 <= r^2,
142
143    which is recognized as convex for r >= 0.
144
145    Parameters
146    ----------
147    r : :class:`variable`
148        A variable.
149    x : list[:class:`variable`]
150        An iterable of variables.
151    """
152    __slots__ = ("_parent",
153                 "_storage_key",
154                 "_active",
155                 "_body",
156                 "_r",
157                 "_x",
158                 "__weakref__")
159    def __init__(self, r, x):
160        super(quadratic, self).__init__()
161        self._r = r
162        self._x = tuple(x)
163        assert isinstance(self._r, IVariable)
164        assert all(isinstance(xi, IVariable)
165                   for xi in self._x)
166
167    @classmethod
168    def as_domain(cls, r, x):
169        """Builds a conic domain. Input arguments take the
170        same form as those of the conic constraint, but in
171        place of each variable, one can optionally supply a
172        constant, linear expression, or None.
173
174        Returns
175        -------
176        block
177            A block object with the core conic constraint
178            (block.q) expressed using auxiliary variables
179            (block.r, block.x) linked to the input arguments
180            through auxiliary constraints (block.c).
181        """
182        b = block()
183        b.r = variable(lb=0)
184        b.x = variable_tuple(
185            [variable() for i in range(len(x))])
186        b.c = _build_linking_constraints([r] + list(x),
187                                         [b.r] + list(b.x))
188        b.q = cls(r=b.r, x=b.x)
189        return b
190
191    @property
192    def r(self):
193        return self._r
194
195    @property
196    def x(self):
197        return self._x
198
199    #
200    # Define the _ConicBase abstract methods
201    #
202
203    def _body_function(self, r, x):
204        """A function that defines the body expression"""
205        return sum(xi**2 for xi in x) - r**2
206
207    def _body_function_variables(self, values=False):
208        """Returns variables in the order they should be
209        passed to the body function. If values is True, then
210        return the current value of each variable in place
211        of the variables themselves."""
212        if not values:
213            return self.r, self.x
214        else:
215            return self.r.value, tuple(xi.value for xi in self.x)
216
217    def check_convexity_conditions(self, relax=False):
218        """Returns True if all convexity conditions for the
219        conic constraint are satisfied. If relax is True,
220        then variable domains are ignored and it is assumed
221        that all variables are continuous."""
222        return (relax or \
223                (self.r.is_continuous() and \
224                 all(xi.is_continuous() for xi in self.x))) and \
225            (self.r.has_lb() and value(self.r.lb) >= 0)
226
227class rotated_quadratic(_ConicBase):
228    """A rotated quadratic conic constraint of the form:
229
230        x[0]^2 + ... + x[n-1]^2 <= 2*r1*r2,
231
232    which is recognized as convex for r1,r2 >= 0.
233
234    Parameters
235    ----------
236    r1 : :class:`variable`
237        A variable.
238    r2 : :class:`variable`
239        A variable.
240    x : list[:class:`variable`]
241        An iterable of variables.
242    """
243    __slots__ = ("_parent",
244                 "_storage_key",
245                 "_active",
246                 "_body",
247                 "_r1",
248                 "_r2",
249                 "_x",
250                 "__weakref__")
251
252    def __init__(self, r1, r2, x):
253        super(rotated_quadratic, self).__init__()
254        self._r1 = r1
255        self._r2 = r2
256        self._x = tuple(x)
257        assert isinstance(self._r1, IVariable)
258        assert isinstance(self._r2, IVariable)
259        assert all(isinstance(xi, IVariable)
260                   for xi in self._x)
261
262    @classmethod
263    def as_domain(cls, r1, r2, x):
264        """Builds a conic domain. Input arguments take the
265        same form as those of the conic constraint, but in
266        place of each variable, one can optionally supply a
267        constant, linear expression, or None.
268
269        Returns
270        -------
271        block
272            A block object with the core conic constraint
273            (block.q) expressed using auxiliary variables
274            (block.r1, block.r2, block.x) linked to the
275            input arguments through auxiliary constraints
276            (block.c).
277        """
278        b = block()
279        b.r1 = variable(lb=0)
280        b.r2 = variable(lb=0)
281        b.x = variable_tuple(
282            [variable() for i in range(len(x))])
283        b.c = _build_linking_constraints([r1,r2] + list(x),
284                                         [b.r1,b.r2] + list(b.x))
285        b.q = cls(r1=b.r1, r2=b.r2, x=b.x)
286        return b
287
288    @property
289    def r1(self):
290        return self._r1
291
292    @property
293    def r2(self):
294        return self._r2
295
296    @property
297    def x(self):
298        return self._x
299
300    #
301    # Define the _ConicBase abstract methods
302    #
303
304    def _body_function(self, r1, r2, x):
305        """A function that defines the body expression"""
306        return sum(xi**2 for xi in x) - 2*r1*r2
307
308    def _body_function_variables(self, values=False):
309        """Returns variables in the order they should be
310        passed to the body function. If values is True, then
311        return the current value of each variable in place
312        of the variables themselves."""
313        if not values:
314            return self.r1, self.r2, self.x
315        else:
316            return self.r1.value, self.r2.value, \
317                tuple(xi.value for xi in self.x)
318
319    def check_convexity_conditions(self, relax=False):
320        """Returns True if all convexity conditions for the
321        conic constraint are satisfied. If relax is True,
322        then variable domains are ignored and it is assumed
323        that all variables are continuous."""
324        return (relax or \
325                (self.r1.is_continuous() and \
326                 self.r2.is_continuous() and \
327                 all(xi.is_continuous() for xi in self.x))) and \
328            (self.r1.has_lb() and value(self.r1.lb) >= 0) and \
329            (self.r2.has_lb() and value(self.r2.lb) >= 0)
330
331class primal_exponential(_ConicBase):
332    """A primal exponential conic constraint of the form:
333
334        x1*exp(x2/x1) <= r,
335
336    which is recognized as convex for x1,r >= 0.
337
338    Parameters
339    ----------
340    r : :class:`variable`
341        A variable.
342    x1 : :class:`variable`
343        A variable.
344    x2 : :class:`variable`
345        A variable.
346    """
347    __slots__ = ("_parent",
348                 "_storage_key",
349                 "_active",
350                 "_body",
351                 "_r",
352                 "_x1",
353                 "_x2",
354                 "__weakref__")
355
356    def __init__(self, r, x1, x2):
357        super(primal_exponential, self).__init__()
358        self._r = r
359        self._x1 = x1
360        self._x2 = x2
361        assert isinstance(self._r, IVariable)
362        assert isinstance(self._x1, IVariable)
363        assert isinstance(self._x2, IVariable)
364
365    @classmethod
366    def as_domain(cls, r, x1, x2):
367        """Builds a conic domain. Input arguments take the
368        same form as those of the conic constraint, but in
369        place of each variable, one can optionally supply a
370        constant, linear expression, or None.
371
372        Returns
373        -------
374        block
375            A block object with the core conic constraint
376            (block.q) expressed using auxiliary variables
377            (block.r, block.x1, block.x2) linked to the
378            input arguments through auxiliary constraints
379            (block.c).
380        """
381        b = block()
382        b.r = variable(lb=0)
383        b.x1 = variable(lb=0)
384        b.x2 = variable()
385        b.c = _build_linking_constraints([r,x1,x2],
386                                         [b.r,b.x1,b.x2])
387        b.q = cls(r=b.r, x1=b.x1, x2=b.x2)
388        return b
389
390    @property
391    def r(self):
392        return self._r
393
394    @property
395    def x1(self):
396        return self._x1
397
398    @property
399    def x2(self):
400        return self._x2
401
402    #
403    # Define the _ConicBase abstract methods
404    #
405
406    def _body_function(self, r, x1, x2):
407        """A function that defines the body expression"""
408        return x1*exp(x2/x1) - r
409
410    def _body_function_variables(self, values=False):
411        """Returns variables in the order they should be
412        passed to the body function. If values is True, then
413        return the current value of each variable in place
414        of the variables themselves."""
415        if not values:
416            return self.r, self.x1, self.x2
417        else:
418            return self.r.value, self.x1.value, self.x2.value
419
420    def check_convexity_conditions(self, relax=False):
421        """Returns True if all convexity conditions for the
422        conic constraint are satisfied. If relax is True,
423        then variable domains are ignored and it is assumed
424        that all variables are continuous."""
425        return (relax or \
426                (self.x1.is_continuous() and \
427                 self.x2.is_continuous() and \
428                 self.r.is_continuous())) and \
429            (self.x1.has_lb() and value(self.x1.lb) >= 0) and \
430            (self.r.has_lb() and value(self.r.lb) >= 0)
431
432class primal_power(_ConicBase):
433    """A primal power conic constraint of the form:
434       sqrt(x[0]^2 + ... + x[n-1]^2) <= (r1^alpha)*(r2^(1-alpha))
435
436    which is recognized as convex for r1,r2 >= 0
437    and 0 < alpha < 1.
438
439    Parameters
440    ----------
441    r1 : :class:`variable`
442        A variable.
443    r2 : :class:`variable`
444        A variable.
445    x : list[:class:`variable`]
446        An iterable of variables.
447    alpha : float, :class:`parameter`, etc.
448        A constant term.
449    """
450    __slots__ = ("_parent",
451                 "_storage_key",
452                 "_active",
453                 "_body",
454                 "_r1",
455                 "_r2",
456                 "_x",
457                 "_alpha",
458                 "__weakref__")
459
460    def __init__(self, r1, r2, x, alpha):
461        super(primal_power, self).__init__()
462        self._r1 = r1
463        self._r2 = r2
464        self._x = tuple(x)
465        self._alpha = alpha
466        assert isinstance(self._r1, IVariable)
467        assert isinstance(self._r2, IVariable)
468        assert all(isinstance(xi, IVariable)
469                   for xi in self._x)
470        if not is_numeric_data(self._alpha):
471            raise TypeError(
472                "The type of the alpha parameter of a conic "
473                "constraint is restricted numeric data or "
474                "objects that store numeric data.")
475
476    @classmethod
477    def as_domain(cls, r1, r2, x, alpha):
478        """Builds a conic domain. Input arguments take the
479        same form as those of the conic constraint, but in
480        place of each variable, one can optionally supply a
481        constant, linear expression, or None.
482
483        Returns
484        -------
485        block
486            A block object with the core conic constraint
487            (block.q) expressed using auxiliary variables
488            (block.r1, block.r2, block.x) linked to the
489            input arguments through auxiliary constraints
490            (block.c).
491        """
492        b = block()
493        b.r1 = variable(lb=0)
494        b.r2 = variable(lb=0)
495        b.x = variable_tuple(
496            [variable() for i in range(len(x))])
497        b.c = _build_linking_constraints([r1,r2] + list(x),
498                                         [b.r1,b.r2] + list(b.x))
499        b.q = cls(r1=b.r1, r2=b.r2, x=b.x, alpha=alpha)
500        return b
501
502    @property
503    def r1(self):
504        return self._r1
505
506    @property
507    def r2(self):
508        return self._r2
509
510    @property
511    def x(self):
512        return self._x
513
514    @property
515    def alpha(self):
516        return self._alpha
517
518    #
519    # Define the _ConicBase abstract methods
520    #
521
522    def _body_function(self, r1, r2, x):
523        """A function that defines the body expression"""
524        alpha = self.alpha
525        return (sum(xi**2 for xi in x)**0.5) - \
526            (r1**alpha) * \
527            (r2**(1-alpha))
528
529    def _body_function_variables(self, values=False):
530        """Returns variables in the order they should be
531        passed to the body function. If values is True, then
532        return the current value of each variable in place
533        of the variables themselves."""
534        if not values:
535            return self.r1, self.r2, self.x
536        else:
537            return self.r1.value, self.r2.value, \
538                tuple(xi.value for xi in self.x)
539
540    def check_convexity_conditions(self, relax=False):
541        """Returns True if all convexity conditions for the
542        conic constraint are satisfied. If relax is True,
543        then variable domains are ignored and it is assumed
544        that all variables are continuous."""
545        alpha = value(self.alpha, exception=False)
546        return (relax or \
547                (self.r1.is_continuous() and \
548                 self.r2.is_continuous() and \
549                 all(xi.is_continuous() for xi in self.x))) and \
550            (self.r1.has_lb() and value(self.r1.lb) >= 0) and \
551            (self.r2.has_lb() and value(self.r2.lb) >= 0) and \
552            ((alpha is not None) and (0 < alpha < 1))
553
554class dual_exponential(_ConicBase):
555    """A dual exponential conic constraint of the form:
556
557        -x2*exp((x1/x2)-1) <= r
558
559    which is recognized as convex for x2 <= 0 and r >= 0.
560
561    Parameters
562    ----------
563    r : :class:`variable`
564        A variable.
565    x1 : :class:`variable`
566        A variable.
567    x2 : :class:`variable`
568        A variable.
569    """
570    __slots__ = ("_parent",
571                 "_storage_key",
572                 "_active",
573                 "_body",
574                 "_r",
575                 "_x1",
576                 "_x2",
577                 "__weakref__")
578
579    def __init__(self, r, x1, x2):
580        super(dual_exponential, self).__init__()
581        self._r = r
582        self._x1 = x1
583        self._x2 = x2
584        assert isinstance(self._r, IVariable)
585        assert isinstance(self._x1, IVariable)
586        assert isinstance(self._x2, IVariable)
587
588    @classmethod
589    def as_domain(cls, r, x1, x2):
590        """Builds a conic domain. Input arguments take the
591        same form as those of the conic constraint, but in
592        place of each variable, one can optionally supply a
593        constant, linear expression, or None.
594
595        Returns
596        -------
597        block
598            A block object with the core conic constraint
599            (block.q) expressed using auxiliary variables
600            (block.r, block.x1, block.x2) linked to the
601            input arguments through auxiliary constraints
602            (block.c).
603        """
604        b = block()
605        b.r = variable(lb=0)
606        b.x1 = variable()
607        b.x2 = variable(ub=0)
608        b.c = _build_linking_constraints([r,x1,x2],
609                                         [b.r,b.x1,b.x2])
610        b.q = cls(r=b.r, x1=b.x1, x2=b.x2)
611        return b
612
613    @property
614    def r(self):
615        return self._r
616
617    @property
618    def x1(self):
619        return self._x1
620
621    @property
622    def x2(self):
623        return self._x2
624
625    #
626    # Define the _ConicBase abstract methods
627    #
628
629    def _body_function(self, r, x1, x2):
630        """A function that defines the body expression"""
631        return -x2*exp((x1/x2) - 1) - r
632
633    def _body_function_variables(self, values=False):
634        """Returns variables in the order they should be
635        passed to the body function. If values is True, then
636        return the current value of each variable in place
637        of the variables themselves."""
638        if not values:
639            return self.r, self.x1, self.x2
640        else:
641            return self.r.value, self.x1.value, self.x2.value
642
643    def check_convexity_conditions(self, relax=False):
644        """Returns True if all convexity conditions for the
645        conic constraint are satisfied. If relax is True,
646        then variable domains are ignored and it is assumed
647        that all variables are continuous."""
648        return (relax or \
649                (self.x1.is_continuous() and \
650                 self.x2.is_continuous() and \
651                 self.r.is_continuous())) and \
652            (self.x2.has_ub() and value(self.x2.ub) <= 0) and \
653            (self.r.has_lb() and value(self.r.lb) >= 0)
654
655class dual_power(_ConicBase):
656    """A dual power conic constraint of the form:
657
658        sqrt(x[0]^2 + ... + x[n-1]^2) <= ((r1/alpha)^alpha) * \
659                                         ((r2/(1-alpha))^(1-alpha))
660
661    which is recognized as convex for r1,r2 >= 0
662    and 0 < alpha < 1.
663
664    Parameters
665    ----------
666    r1 : :class:`variable`
667        A variable.
668    r2 : :class:`variable`
669        A variable.
670    x : list[:class:`variable`]
671        An iterable of variables.
672    alpha : float, :class:`parameter`, etc.
673        A constant term.
674    """
675    __slots__ = ("_parent",
676                 "_storage_key",
677                 "_active",
678                 "_body",
679                 "_r1",
680                 "_r2",
681                 "_x",
682                 "_alpha",
683                 "__weakref__")
684
685    def __init__(self, r1, r2, x, alpha):
686        super(dual_power, self).__init__()
687        self._r1 = r1
688        self._r2 = r2
689        self._x = tuple(x)
690        self._alpha = alpha
691        assert isinstance(self._r1, IVariable)
692        assert isinstance(self._r2, IVariable)
693        assert all(isinstance(xi, IVariable)
694                   for xi in self._x)
695        if not is_numeric_data(self._alpha):
696            raise TypeError(
697                "The type of the alpha parameter of a conic "
698                "constraint is restricted numeric data or "
699                "objects that store numeric data.")
700
701    @classmethod
702    def as_domain(cls, r1, r2, x, alpha):
703        """Builds a conic domain. Input arguments take the
704        same form as those of the conic constraint, but in
705        place of each variable, one can optionally supply a
706        constant, linear expression, or None.
707
708        Returns
709        -------
710        block
711            A block object with the core conic constraint
712            (block.q) expressed using auxiliary variables
713            (block.r1, block.r2, block.x) linked to the
714            input arguments through auxiliary constraints
715            (block.c).
716        """
717        b = block()
718        b.r1 = variable(lb=0)
719        b.r2 = variable(lb=0)
720        b.x = variable_tuple(
721            [variable() for i in range(len(x))])
722        b.c = _build_linking_constraints([r1,r2] + list(x),
723                                         [b.r1,b.r2] + list(b.x))
724        b.q = cls(r1=b.r1, r2=b.r2, x=b.x, alpha=alpha)
725        return b
726
727    @property
728    def r1(self):
729        return self._r1
730
731    @property
732    def r2(self):
733        return self._r2
734
735    @property
736    def x(self):
737        return self._x
738
739    @property
740    def alpha(self):
741        return self._alpha
742
743    #
744    # Define the _ConicBase abstract methods
745    #
746
747    def _body_function(self, r1, r2, x):
748        """A function that defines the body expression"""
749        alpha = self.alpha
750        return (sum(xi**2 for xi in x)**0.5) - \
751            ((r1/alpha)**alpha) * \
752            ((r2/(1-alpha))**(1-alpha))
753
754    def _body_function_variables(self, values=False):
755        """Returns variables in the order they should be
756        passed to the body function. If values is True, then
757        return the current value of each variable in place
758        of the variables themselves."""
759        if not values:
760            return self.r1, self.r2, self.x
761        else:
762            return self.r1.value, self.r2.value, \
763                tuple(xi.value for xi in self.x)
764
765    def check_convexity_conditions(self, relax=False):
766        """Returns True if all convexity conditions for the
767        conic constraint are satisfied. If relax is True,
768        then variable domains are ignored and it is assumed
769        that all variables are continuous."""
770        alpha = value(self.alpha, exception=False)
771        return (relax or \
772                (self.r1.is_continuous() and \
773                 self.r2.is_continuous() and \
774                 all(xi.is_continuous() for xi in self.x))) and \
775            (self.r1.has_lb() and value(self.r1.lb) >= 0) and \
776            (self.r2.has_lb() and value(self.r2.lb) >= 0) and \
777            ((alpha is not None) and (0 < alpha < 1))
778