Coverage for picos/constraints/con_affine.py: 96.72%

122 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-03-26 07:46 +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 

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 @property 

80 def smaller(self): 

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

82 

83 The smaller-or-equal side expression in case of an inequality, otherwise 

84 the left hand side. 

85 """ 

86 return self.rhs if self.relation == self.GE else self.lhs 

87 

88 @property 

89 def greater(self): 

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

91 

92 The greater-or-equal side expression in case of an inequality, otherwise 

93 the right hand side. 

94 """ 

95 return self.lhs if self.relation == self.GE else self.rhs 

96 

97 @cached_property 

98 def lmr(self): 

99 """Left hand side minus right hand side.""" 

100 return self.lhs - self.rhs 

101 

102 @cached_property 

103 def rml(self): 

104 """Right hand side minus left hand side.""" 

105 return self.rhs - self.lhs 

106 

107 @property 

108 def le0(self): 

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

110 

111 The expression posed to be less than or equal to zero in case of an 

112 inequality, otherwise the left hand side minus the right hand side. 

113 """ 

114 if self.relation == self.GE: 

115 return self.rml 

116 else: 

117 return self.lmr 

118 

119 @property 

120 def ge0(self): 

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

122 

123 The expression posed to be greater than or equal to zero in case of an 

124 inequality, otherwise the left hand side minus the right hand side. 

125 """ 

126 if self.relation == self.LE: 

127 return self.rml 

128 else: 

129 return self.lmr 

130 

131 @classmethod 

132 def _cost(cls, subtype): 

133 return subtype.dim 

134 

135 def _expression_names(self): 

136 yield "lhs" 

137 yield "rhs" 

138 

139 def _str(self): 

140 if self.relation == self.LE: 

141 return glyphs.le(self.lhs.string, self.rhs.string) 

142 elif self.relation == self.GE: 

143 return glyphs.ge(self.lhs.string, self.rhs.string) 

144 else: 

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

146 

147 def _get_size(self): 

148 return self.lhs.size 

149 

150 def _get_slack(self): 

151 if self.relation == self.LE: 

152 delta = self.rml.safe_value 

153 else: 

154 delta = self.lmr.safe_value 

155 

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

157 

158 def bounded_linear_form(self): 

159 """Bounded linear form of the constraint. 

160 

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

162 and a constant bound on the right hand side. 

163 

164 :returns: A pair ``(linear, bound)`` where ``linear`` is a pure linear 

165 expression and ``bound`` is a constant expression. 

166 """ 

167 linear = self.lmr 

168 bound = -linear.cst 

169 linear = linear + bound 

170 

171 return (linear, bound) 

172 

173 

174class ComplexAffineConstraint(ConicConstraint): 

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

176 

177 class RealConversion(ConstraintConversion): 

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

179 

180 @classmethod 

181 def predict(cls, subtype, options): 

182 """Implement :meth:`~.constraint.ConstraintConversion.predict`.""" 

183 yield ("con", 

184 AffineConstraint.make_type(dim=2*subtype.dim, eq=True), 1) 

185 

186 @classmethod 

187 def convert(cls, con, options): 

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

189 from ..modeling import Problem 

190 

191 P = Problem() 

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

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

194 

195 return P 

196 

197 @classmethod 

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

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

200 assert len(auxConDuals) == 1 

201 

202 auxConDual = auxConDuals[0] 

203 if auxConDual is None: 

204 return None 

205 else: 

206 n = auxConDual.size[0] // 2 

207 return auxConDual[:n, :] + 1j*auxConDual[n:, :] 

208 

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

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

211 

212 :param ~picos.expressions.AffineExpression lhs: 

213 Left hand side expression. 

214 :param ~picos.expressions.AffineExpression rhs: 

215 Right hand side expression. 

216 :param str customString: 

217 Optional string description. 

218 """ 

219 from ..expressions import ComplexAffineExpression 

220 

221 assert isinstance(lhs, ComplexAffineExpression) 

222 assert isinstance(rhs, ComplexAffineExpression) 

223 assert lhs.size == rhs.size 

224 

225 self.lhs = lhs 

226 self.rhs = rhs 

227 

228 super(ComplexAffineConstraint, self).__init__( 

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

230 

231 @cached_property 

232 def conic_membership_form(self): 

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

234 from ..expressions import ZeroSpace 

235 return self.lhs - self.rhs, ZeroSpace() 

236 

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

238 

239 def _subtype(self): 

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

241 

242 @classmethod 

243 def _cost(cls, subtype): 

244 return 2*subtype.dim 

245 

246 def _expression_names(self): 

247 yield "lhs" 

248 yield "rhs" 

249 

250 def _str(self): 

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

252 

253 def _get_size(self): 

254 return self.lhs.size 

255 

256 def _get_slack(self): 

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

258 

259 

260# -------------------------------------- 

261__all__ = api_end(_API_START, globals())