Coverage for picos/constraints/con_lmi.py: 87.29%
118 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) 2018-2019 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"""Linear matrix inequalities."""
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 LMIConstraint(ConicConstraint):
33 """Linear matrix inequality.
35 An inequality with respect to the positive semidefinite cone, also known as
36 a Linear Matrix Inequality (LMI) or an SDP constraint.
37 """
39 def __init__(self, lhs, relation, rhs, customString=None):
40 """Construct a :class:`LMIConstraint`.
42 :param ~picos.expressions.AffineExpression lhs:
43 Left hand side expression.
44 :param str relation:
45 Constraint relation symbol.
46 :param ~picos.expressions.AffineExpression rhs:
47 Right hand side expression.
48 :param str customString:
49 Optional string description.
50 """
51 from ..expressions import AffineExpression
53 required_type = self._required_type()
55 assert isinstance(lhs, required_type)
56 assert isinstance(rhs, required_type)
57 assert relation in self.LE + self.GE
59 if lhs.shape != rhs.shape:
60 raise ValueError("Failed to form a constraint: "
61 "Expressions have incompatible dimensions.")
63 if lhs.shape[0] != lhs.shape[1]:
64 raise ValueError("Failed to form a constraint: "
65 "LMI expressions are not square.")
67 self.lhs = lhs
68 self.rhs = rhs
69 self.relation = relation
71 psd = self.psd
72 if not psd.hermitian:
73 needed = "symmetric" if required_type is AffineExpression \
74 else "hermitian"
76 raise ValueError("Failed to form a constraint: {} is not "
77 "necessarily {}. Consider a constraint on {} instead.".format(
78 psd.string, needed, glyphs.Tr("{}.hermitianized")(psd.string)))
80 super(LMIConstraint, self).__init__(
81 self._get_type_term(), customString, printSize=True)
83 def _get_type_term(self):
84 return "LMI"
86 def _required_type(self):
87 from ..expressions import AffineExpression
89 return AffineExpression
91 @cached_property
92 def semidefVar(self):
93 """The variable posed positive semidefinite, or :obj:`None`."""
94 from ..expressions import HermitianVariable, SymmetricVariable
95 from ..expressions.data import cvxopt_equals
97 psd = self.psd
99 if len(psd._linear_coefs) == 1 and not psd._constant_coef:
100 var, coef = next(iter(psd._linear_coefs.items()))
102 if isinstance(var, (SymmetricVariable, HermitianVariable)) \
103 and cvxopt_equals(coef, var._vec.identity,
104 relTol=settings.RELATIVE_HERMITIANNESS_TOLERANCE):
105 return var
107 return None
109 @property
110 def smaller(self):
111 """The smaller-or-equal side expression."""
112 return self.rhs if self.relation == self.GE else self.lhs
114 @property
115 def greater(self):
116 """The greater-or-equal side expression."""
117 return self.lhs if self.relation == self.GE else self.rhs
119 @cached_property
120 def psd(self):
121 """The matrix expression posed to be positive semidefinite."""
122 if self.relation == self.GE:
123 return self.lhs - self.rhs
124 else:
125 return self.rhs - self.lhs
127 @cached_property
128 def nsd(self):
129 """The matrix expression posed to be negative semidefinite."""
130 if self.relation == self.GE:
131 return self.rhs - self.lhs
132 else:
133 return self.lhs - self.rhs
135 nnd = psd
136 npd = nsd
138 @cached_property
139 def conic_membership_form(self):
140 """Implement for :class:`~.constraint.ConicConstraint`."""
141 from ..expressions import PositiveSemidefiniteCone
143 element = self.psd.svec
144 return element, PositiveSemidefiniteCone(dim=len(element))
146 Subtype = namedtuple("Subtype", ("diag",))
148 def _subtype(self):
149 return self.Subtype(self.lhs.shape[0])
151 @classmethod
152 def _cost(cls, subtype):
153 n = subtype.diag
154 return n*(n + 1)//2
156 def _expression_names(self):
157 yield "lhs"
158 yield "rhs"
160 def _str(self):
161 if self.relation == self.LE:
162 return glyphs.psdle(self.lhs.string, self.rhs.string)
163 else:
164 return glyphs.psdge(self.lhs.string, self.rhs.string)
166 def _get_size(self):
167 return self.lhs.shape
169 def _get_slack(self):
170 return self.psd.safe_value
173class ComplexLMIConstraint(LMIConstraint):
174 """Complex linear matrix inequality."""
176 class RealConversion(ConstraintConversion):
177 """Complex LMI to real LMI conversion."""
179 @classmethod
180 def predict(cls, subtype, options):
181 """Implement :meth:`~.constraint.ConstraintConversion.predict`."""
182 n = subtype.diag
184 yield ("con", LMIConstraint.make_type(diag=2*n), 1)
186 @classmethod
187 def convert(cls, con, options):
188 """Implement :meth:`~.constraint.ConstraintConversion.convert`."""
189 from ..expressions.algebra import block
190 from ..modeling import Problem
192 P = Problem()
193 Z = con.psd
194 P.add_constraint(block([[Z.real, -Z.imag], [Z.imag, Z.real]]) >> 0)
196 return P
198 @classmethod
199 def dual(cls, auxVarPrimals, auxConDuals, options):
200 """Implement :meth:`~.constraint.ConstraintConversion.dual`."""
201 assert len(auxConDuals) == 1
203 auxConDual = auxConDuals[0]
204 if auxConDual is None:
205 return None
206 else:
207 assert auxConDual.size[0] == auxConDual.size[1]
208 n = auxConDual.size[0] // 2
209 assert 2*n == auxConDual.size[0]
210 A = auxConDual[:n, :n]
211 B = auxConDual[:n, n:]
212 D = auxConDual[n:, n:]
213 return (A + 1j*B) + (D + 1j*B).H
215 def _get_type_term(self):
216 return "Complex LMI"
218 def _required_type(self):
219 from ..expressions import ComplexAffineExpression
221 return ComplexAffineExpression
223 @classmethod
224 def _cost(cls, subtype):
225 return subtype.diag**2
228# --------------------------------------
229__all__ = api_end(_API_START, globals())