Coverage for picos/constraints/con_affine.py: 96.24%
133 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-12 07:53 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-12 07:53 +0000
1# ------------------------------------------------------------------------------
2# Copyright (C) 2018-2022 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"""Affine constraint types."""
21from collections import namedtuple
23from .. import glyphs, settings
24from ..apidoc import api_end, api_start
25from ..caching import cached_property
26from .constraint import ConicConstraint, ConstraintConversion
28_API_START = api_start(globals())
29# -------------------------------
32class AffineConstraint(ConicConstraint):
33 """An equality or inequality between two affine expressions."""
35 def __init__(self, lhs, relation, rhs, customString=None):
36 """Construct an :class:`AffineConstraint`.
38 :param ~picos.expressions.AffineExpression lhs:
39 Left hand side expression.
40 :param str relation:
41 Constraint relation symbol.
42 :param ~picos.expressions.AffineExpression rhs:
43 Right hand side expression.
44 :param str customString:
45 Optional string description.
46 """
47 from ..expressions import AffineExpression
49 assert isinstance(lhs, AffineExpression)
50 assert isinstance(rhs, AffineExpression)
51 assert relation in self.LE + self.GE + self.EQ
52 assert lhs.size == rhs.size
54 self.lhs = lhs
55 self.rhs = rhs
56 self.relation = relation
58 super(AffineConstraint, self).__init__(
59 "Affine", customString, printSize=True)
61 @cached_property
62 def conic_membership_form(self):
63 """Implement for :class:`~.constraint.ConicConstraint`."""
64 from ..expressions import NonnegativeOrthant, ZeroSpace
66 element = self.ge0.vec
67 dim = len(element)
69 if self.relation == self.EQ:
70 return element, ZeroSpace(dim=dim)
71 else:
72 return element, NonnegativeOrthant(dim=dim)
74 Subtype = namedtuple("Subtype", ("dim", "eq"))
76 def _subtype(self):
77 return self.Subtype(len(self.lhs), self.relation == self.EQ)
79 @cached_property
80 def nnVar(self):
81 """The variable posed nonnegative, or :obj:`None`."""
82 from ..expressions import RealVariable
83 from ..expressions.data import cvxopt_equals
85 ge0 = self.ge0
87 if self.relation == self.EQ:
88 return None
90 if len(ge0._linear_coefs) == 1 and not ge0._constant_coef:
91 var, coef = next(iter(ge0._linear_coefs.items()))
93 if isinstance(var, RealVariable) \
94 and cvxopt_equals(coef, var._vec.identity,
95 relTol=settings.RELATIVE_HERMITIANNESS_TOLERANCE):
96 return var
98 return None
100 @property
101 def smaller(self):
102 """Smaller-or-equal side of the constraint.
104 The smaller-or-equal side expression in case of an inequality, otherwise
105 the left hand side.
106 """
107 return self.rhs if self.relation == self.GE else self.lhs
109 @property
110 def greater(self):
111 """Greater-or-equal side of the constraint.
113 The greater-or-equal side expression in case of an inequality, otherwise
114 the right hand side.
115 """
116 return self.lhs if self.relation == self.GE else self.rhs
118 @cached_property
119 def lmr(self):
120 """Left hand side minus right hand side."""
121 return self.lhs - self.rhs
123 @cached_property
124 def rml(self):
125 """Right hand side minus left hand side."""
126 return self.rhs - self.lhs
128 @property
129 def le0(self):
130 """Expression constrained to be lower than or equal to zero.
132 The expression posed to be less than or equal to zero in case of an
133 inequality, otherwise the left hand side minus the right hand side.
134 """
135 if self.relation == self.GE:
136 return self.rml
137 else:
138 return self.lmr
140 @property
141 def ge0(self):
142 """Expression constrained to be greater than or equal to zero.
144 The expression posed to be greater than or equal to zero in case of an
145 inequality, otherwise the left hand side minus the right hand side.
146 """
147 if self.relation == self.LE:
148 return self.rml
149 else:
150 return self.lmr
152 @classmethod
153 def _cost(cls, subtype):
154 return subtype.dim
156 def _expression_names(self):
157 yield "lhs"
158 yield "rhs"
160 def _str(self):
161 if self.relation == self.LE:
162 return glyphs.le(self.lhs.string, self.rhs.string)
163 elif self.relation == self.GE:
164 return glyphs.ge(self.lhs.string, self.rhs.string)
165 else:
166 return glyphs.eq(self.lhs.string, self.rhs.string)
168 def _get_size(self):
169 return self.lhs.size
171 def _get_slack(self):
172 if self.relation == self.LE:
173 delta = self.rml.safe_value
174 else:
175 delta = self.lmr.safe_value
177 return -abs(delta) if self.relation == self.EQ else delta
179 def bounded_linear_form(self):
180 """Bounded linear form of the constraint.
182 Separates the constraint into a linear function on the left hand side
183 and a constant bound on the right hand side.
185 :returns: A pair ``(linear, bound)`` where ``linear`` is a pure linear
186 expression and ``bound`` is a constant expression.
187 """
188 linear = self.lmr
189 bound = -linear.cst
190 linear = linear + bound
192 return (linear, bound)
195class ComplexAffineConstraint(ConicConstraint):
196 """An equality between affine expressions, at least one being complex."""
198 class RealConversion(ConstraintConversion):
199 """Complex affine equality to real affine equality conversion."""
201 @classmethod
202 def predict(cls, subtype, options):
203 """Implement :meth:`~.constraint.ConstraintConversion.predict`."""
204 yield ("con",
205 AffineConstraint.make_type(dim=2*subtype.dim, eq=True), 1)
207 @classmethod
208 def convert(cls, con, options):
209 """Implement :meth:`~.constraint.ConstraintConversion.convert`."""
210 from ..modeling import Problem
212 P = Problem()
213 P.add_constraint((con.lhs.real // con.lhs.imag)
214 == (con.rhs.real // con.rhs.imag))
216 return P
218 @classmethod
219 def dual(cls, auxVarPrimals, auxConDuals, options):
220 """Implement :meth:`~.constraint.ConstraintConversion.dual`."""
221 assert len(auxConDuals) == 1
223 auxConDual = auxConDuals[0]
224 if auxConDual is None:
225 return None
226 else:
227 n = auxConDual.size[0] // 2
228 return auxConDual[:n, :] + 1j*auxConDual[n:, :]
230 def __init__(self, lhs, rhs, customString=None):
231 """Construct a :class:`ComplexAffineConstraint`.
233 :param ~picos.expressions.AffineExpression lhs:
234 Left hand side expression.
235 :param ~picos.expressions.AffineExpression rhs:
236 Right hand side expression.
237 :param str customString:
238 Optional string description.
239 """
240 from ..expressions import ComplexAffineExpression
242 assert isinstance(lhs, ComplexAffineExpression)
243 assert isinstance(rhs, ComplexAffineExpression)
244 assert lhs.size == rhs.size
246 self.lhs = lhs
247 self.rhs = rhs
249 super(ComplexAffineConstraint, self).__init__(
250 "Complex Equality", customString, printSize=True)
252 @cached_property
253 def conic_membership_form(self):
254 """Implement for :class:`~.constraint.ConicConstraint`."""
255 from ..expressions import ZeroSpace
256 return self.lhs - self.rhs, ZeroSpace()
258 Subtype = namedtuple("Subtype", ("dim",))
260 def _subtype(self):
261 return self.Subtype(len(self.lhs))
263 @classmethod
264 def _cost(cls, subtype):
265 return 2*subtype.dim
267 def _expression_names(self):
268 yield "lhs"
269 yield "rhs"
271 def _str(self):
272 return glyphs.eq(self.lhs.string, self.rhs.string)
274 def _get_size(self):
275 return self.lhs.size
277 def _get_slack(self):
278 return -abs(self.lhs.safe_value - self.rhs.safe_value)
281# --------------------------------------
282__all__ = api_end(_API_START, globals())