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

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# ------------------------------------------------------------------------------ 

18 

19"""Affine constraint types.""" 

20 

21from collections import namedtuple 

22 

23from .. import glyphs, settings 

24from ..apidoc import api_end, api_start 

25from ..caching import cached_property 

26from .constraint import ConicConstraint, ConstraintConversion 

27 

28_API_START = api_start(globals()) 

29# ------------------------------- 

30 

31 

32class AffineConstraint(ConicConstraint): 

33 """An equality or inequality between two affine expressions.""" 

34 

35 def __init__(self, lhs, relation, rhs, customString=None): 

36 """Construct an :class:`AffineConstraint`. 

37 

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 

48 

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 

53 

54 self.lhs = lhs 

55 self.rhs = rhs 

56 self.relation = relation 

57 

58 super(AffineConstraint, self).__init__( 

59 "Affine", customString, printSize=True) 

60 

61 @cached_property 

62 def conic_membership_form(self): 

63 """Implement for :class:`~.constraint.ConicConstraint`.""" 

64 from ..expressions import NonnegativeOrthant, ZeroSpace 

65 

66 element = self.ge0.vec 

67 dim = len(element) 

68 

69 if self.relation == self.EQ: 

70 return element, ZeroSpace(dim=dim) 

71 else: 

72 return element, NonnegativeOrthant(dim=dim) 

73 

74 Subtype = namedtuple("Subtype", ("dim", "eq")) 

75 

76 def _subtype(self): 

77 return self.Subtype(len(self.lhs), self.relation == self.EQ) 

78 

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 

84 

85 ge0 = self.ge0 

86 

87 if self.relation == self.EQ: 

88 return None 

89 

90 if len(ge0._linear_coefs) == 1 and not ge0._constant_coef: 

91 var, coef = next(iter(ge0._linear_coefs.items())) 

92 

93 if isinstance(var, RealVariable) \ 

94 and cvxopt_equals(coef, var._vec.identity, 

95 relTol=settings.RELATIVE_HERMITIANNESS_TOLERANCE): 

96 return var 

97 

98 return None 

99 

100 @property 

101 def smaller(self): 

102 """Smaller-or-equal side of the constraint. 

103 

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 

108 

109 @property 

110 def greater(self): 

111 """Greater-or-equal side of the constraint. 

112 

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 

117 

118 @cached_property 

119 def lmr(self): 

120 """Left hand side minus right hand side.""" 

121 return self.lhs - self.rhs 

122 

123 @cached_property 

124 def rml(self): 

125 """Right hand side minus left hand side.""" 

126 return self.rhs - self.lhs 

127 

128 @property 

129 def le0(self): 

130 """Expression constrained to be lower than or equal to zero. 

131 

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 

139 

140 @property 

141 def ge0(self): 

142 """Expression constrained to be greater than or equal to zero. 

143 

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 

151 

152 @classmethod 

153 def _cost(cls, subtype): 

154 return subtype.dim 

155 

156 def _expression_names(self): 

157 yield "lhs" 

158 yield "rhs" 

159 

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) 

167 

168 def _get_size(self): 

169 return self.lhs.size 

170 

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 

176 

177 return -abs(delta) if self.relation == self.EQ else delta 

178 

179 def bounded_linear_form(self): 

180 """Bounded linear form of the constraint. 

181 

182 Separates the constraint into a linear function on the left hand side 

183 and a constant bound on the right hand side. 

184 

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 

191 

192 return (linear, bound) 

193 

194 

195class ComplexAffineConstraint(ConicConstraint): 

196 """An equality between affine expressions, at least one being complex.""" 

197 

198 class RealConversion(ConstraintConversion): 

199 """Complex affine equality to real affine equality conversion.""" 

200 

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) 

206 

207 @classmethod 

208 def convert(cls, con, options): 

209 """Implement :meth:`~.constraint.ConstraintConversion.convert`.""" 

210 from ..modeling import Problem 

211 

212 P = Problem() 

213 P.add_constraint((con.lhs.real // con.lhs.imag) 

214 == (con.rhs.real // con.rhs.imag)) 

215 

216 return P 

217 

218 @classmethod 

219 def dual(cls, auxVarPrimals, auxConDuals, options): 

220 """Implement :meth:`~.constraint.ConstraintConversion.dual`.""" 

221 assert len(auxConDuals) == 1 

222 

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:, :] 

229 

230 def __init__(self, lhs, rhs, customString=None): 

231 """Construct a :class:`ComplexAffineConstraint`. 

232 

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 

241 

242 assert isinstance(lhs, ComplexAffineExpression) 

243 assert isinstance(rhs, ComplexAffineExpression) 

244 assert lhs.size == rhs.size 

245 

246 self.lhs = lhs 

247 self.rhs = rhs 

248 

249 super(ComplexAffineConstraint, self).__init__( 

250 "Complex Equality", customString, printSize=True) 

251 

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() 

257 

258 Subtype = namedtuple("Subtype", ("dim",)) 

259 

260 def _subtype(self): 

261 return self.Subtype(len(self.lhs)) 

262 

263 @classmethod 

264 def _cost(cls, subtype): 

265 return 2*subtype.dim 

266 

267 def _expression_names(self): 

268 yield "lhs" 

269 yield "rhs" 

270 

271 def _str(self): 

272 return glyphs.eq(self.lhs.string, self.rhs.string) 

273 

274 def _get_size(self): 

275 return self.lhs.size 

276 

277 def _get_slack(self): 

278 return -abs(self.lhs.safe_value - self.rhs.safe_value) 

279 

280 

281# -------------------------------------- 

282__all__ = api_end(_API_START, globals())