Coverage for picos/constraints/uncertain/ucon_ws_pwl.py: 28.41%
88 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-15 14:21 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-15 14:21 +0000
1# ------------------------------------------------------------------------------
2# Copyright (C) 2020 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"""Implements :class:`WassersteinAmbiguousExtremumAffineConstraint`."""
21from collections import namedtuple
23from ... import glyphs
24from ...apidoc import api_end, api_start
25from ...caching import cached_property
26from ..constraint import Constraint, ConstraintConversion
28_API_START = api_start(globals())
29# -------------------------------
32class WassersteinAmbiguousExtremumAffineConstraint(Constraint):
33 """A bound on a W_1-ambiguous expected value of a piecewise function."""
35 class DistributionallyRobustConversion(ConstraintConversion):
36 """Distributionally robust counterpart conversion."""
38 @classmethod
39 def predict(cls, subtype, options):
40 """Implement :meth:`~.constraint.ConstraintConversion.predict`."""
41 from ...expressions import RealVariable
42 from .. import AffineConstraint, SOCConstraint
44 k = subtype.extremum_argnum
45 m = subtype.universe_subtype.sample_dim
46 N = subtype.universe_subtype.sample_num
48 yield ("var", RealVariable.make_var_type(dim=1, bnd=0), 1) # gamma
49 yield ("var", RealVariable.make_var_type(dim=N, bnd=0), 1) # s
51 yield ("con", AffineConstraint.make_type(dim=1, eq=False), 1)
52 yield ("con", AffineConstraint.make_type(dim=k, eq=False), N)
53 yield ("con", SOCConstraint.make_type(argdim=m), k)
55 @classmethod
56 def convert(cls, con, options):
57 """Implement :meth:`~.constraint.ConstraintConversion.convert`."""
58 # The conversion recipe is found in "Robust conic optimization in
59 # Python" (Stahlberg 2020) and is an application of a result in
60 # "Data-driven distributionally robust optimization using the
61 # Wasserstein metric: Performance guarantees and tractable
62 # reformulations" (Esfahani Mohajerin and Kuhn 2018).
63 from ...expressions import (
64 Constant, RandomMaximumAffine, RealVariable, SecondOrderCone)
65 from ...expressions.algebra import block
66 from ...modeling import Problem
68 # Load the constraint in maximum form.
69 con = con.maximum_form
70 assert isinstance(con.extremum, RandomMaximumAffine)
71 assert con.relation == con.LE
72 k = con.extremum.argnum
73 K = range(k)
75 # Load the ambiguity set.
76 WAS = con.extremum.universe
77 xi = WAS.parameter
78 S = WAS.samples
79 m = S.dim
80 N = S.num
81 w = WAS.weights
82 eps = WAS.eps
84 # Load the uncertain extremum as max(h[i].T*xi + eta[i] for i in K).
85 zero = Constant(0, shape=(1, m))
86 hT, eta = zip(*(
87 (zero, a) if a.certain else a.factor_out(xi)
88 for a in con.extremum.expressions))
90 # Stack the h[i].T and eta[i] vertically and to allow N many
91 # k-dimensional constraints as opposed to N*k scalar constraints.
92 # TODO: Consider adding picos.algebra.hcat and picos.algebra.vcat.
93 hTs = block([[hT[i]] for i in K])
94 etas = block([[eta[i]] for i in K])
96 # Load the upper bound.
97 omega = con.rhs
99 P = Problem()
101 gamma = RealVariable("__gamma")
102 s = RealVariable("__s", N)
104 P.add_constraint(gamma*eps + w.T*s <= omega)
106 for j in range(N):
107 P.add_constraint(hTs*S[j] + etas <= s[j].dupvec(k))
109 for i in K:
110 P.add_constraint((gamma & hT[i]) << SecondOrderCone())
112 return P
114 def __init__(self, extremum, relation, rhs):
115 """Construct a :class:`WassersteinAmbiguousExtremumAffineConstraint`.
117 :param ~picos.expressions.RandomExtremumAffine extremum:
118 Left hand side expression.
119 :param str relation:
120 Constraint relation symbol.
121 :param ~picos.expressions.AffineExpression rhs:
122 Right hand side expression.
123 """
124 from ...expressions import (
125 AffineExpression, RandomMaximumAffine, RandomMinimumAffine)
127 if relation == self.LE:
128 assert isinstance(extremum, RandomMaximumAffine)
129 else:
130 assert isinstance(extremum, RandomMinimumAffine)
131 assert isinstance(rhs, AffineExpression)
132 assert rhs.scalar
134 self.extremum = extremum
135 self.relation = relation
136 self.rhs = rhs
138 super(WassersteinAmbiguousExtremumAffineConstraint, self).__init__(
139 "Wasserstein-ambiguous Piecewise Linear Expectation",
140 printSize=True)
142 @cached_property
143 def maximum_form(self):
144 """The constraint posed as an upper bound on an expected maximum."""
145 if self.relation == self.LE:
146 return self
147 else:
148 return self.__class__(-self.extremum, self.LE, -self.rhs)
150 Subtype = namedtuple("Subtype", (
151 "extremum_argnum",
152 "universe_subtype"))
154 def _subtype(self):
155 return self.Subtype(
156 self.extremum.argnum,
157 self.extremum.universe.subtype)
159 @classmethod
160 def _cost(cls, subtype):
161 return float("inf")
163 def _expression_names(self):
164 yield "extremum"
165 yield "rhs"
167 def _str(self):
168 if self.relation == self.LE:
169 return glyphs.le(
170 self.extremum.worst_case_string("max"), self.rhs.string)
171 else:
172 return glyphs.ge(
173 self.extremum.worst_case_string("min"), self.rhs.string)
175 def _get_size(self):
176 return (1, 1)
178 def _get_slack(self):
179 if self.relation == self.LE:
180 return self.rhs.safe_value - self.extremum.worst_case_value("max")
181 else:
182 return self.extremum.worst_case_value("min") - self.rhs.safe_value
185# --------------------------------------
186__all__ = api_end(_API_START, globals())