Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# ------------------------------------------------------------------------------ 

2# Copyright (C) 2012-2019 Guillaume Sagnol 

3# Copyright (C) 2018-2019 Maximilian Stahlberg 

4# 

5# This file is part of PICOS. 

6# 

7# PICOS is free software: you can redistribute it and/or modify it under the 

8# terms of the GNU General Public License as published by the Free Software 

9# Foundation, either version 3 of the License, or (at your option) any later 

10# version. 

11# 

12# PICOS is distributed in the hope that it will be useful, but WITHOUT ANY 

13# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 

14# A PARTICULAR PURPOSE. See the GNU General Public License for more details. 

15# 

16# You should have received a copy of the GNU General Public License along with 

17# this program. If not, see <http://www.gnu.org/licenses/>. 

18# ------------------------------------------------------------------------------ 

19 

20"""Implementation of :class:`PowerTraceConstraint`.""" 

21 

22import math 

23from collections import namedtuple 

24 

25import cvxopt as cvx 

26 

27from .. import glyphs 

28from ..apidoc import api_end, api_start 

29from .constraint import Constraint, ConstraintConversion 

30 

31_API_START = api_start(globals()) 

32# ------------------------------- 

33 

34 

35class PowerTraceConstraint(Constraint): 

36 """Bound on the trace over the :math:`p`-th power of a matrix. 

37 

38 For scalar expressions, this is simply a bound on their :math:`p`-th power. 

39 """ 

40 

41 class Conversion(ConstraintConversion): 

42 """Bound on the :math:`p`-th power of a trace constraint conversion. 

43 

44 The conversion is based on 

45 `this paper <http://nbn-resolving.de/urn:nbn:de:0297-zib-17511>`_. 

46 """ 

47 

48 @classmethod 

49 def _count_number_tree_node_types(cls, x): 

50 """Count number of conversion tree nodes. 

51 

52 Consider a binary tree with x[i] leaves of type i, arranged from 

53 left to right, with sum(x) a power of two. A node of the tree is of 

54 type i if its 2 parents are of type i; otherwise, a new type is 

55 created for this node. This function counts the number of additional 

56 types we need to create while growing the tree. 

57 """ 

58 x = [xi for xi in x if xi != 0] 

59 sum_x = sum(x) 

60 

61 # We have reached the tree root. Stop the recursion. 

62 if sum_x == 1: 

63 return 0 

64 

65 # Make sure x is a power of two. 

66 _log2_sum_x = math.log(sum_x, 2) 

67 assert _log2_sum_x == int(_log2_sum_x) 

68 

69 new_x = [] 

70 new_t = 0 

71 s = 0 

72 offset = 0 

73 

74 # Compute the vector new_x of types at next level. 

75 for x_i in x: 

76 s += x_i 

77 

78 if s % 2 == 0: 

79 if x_i - offset >= 2: 

80 new_x.append((x_i - offset) // 2) 

81 

82 offset = 0 

83 else: 

84 if x_i - offset >= 2: 

85 new_x.extend([(x_i - offset) // 2, 1]) 

86 elif x_i - offset == 1: 

87 new_x.append(1) 

88 elif x_i - offset == 0: 

89 assert False, "Unexpected case." 

90 

91 offset = 1 

92 new_t += 1 

93 

94 assert 2*sum(new_x) == sum_x 

95 

96 return new_t + cls._count_number_tree_node_types(new_x) 

97 

98 @staticmethod 

99 def _np2(n): 

100 """Compute the smallest power of two that is an upper bound.""" 

101 return 2**int(math.ceil(math.log(n, 2))) 

102 

103 @classmethod 

104 def predict(cls, subtype, options): 

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

106 from ..expressions import (HermitianVariable, RealVariable, 

107 SymmetricVariable) 

108 from . import (AffineConstraint, ComplexLMIConstraint, 

109 RSOCConstraint, LMIConstraint) 

110 

111 n, num, den, hasM, complex = subtype 

112 

113 if num > den > 0: 

114 x = [den, cls._np2(num) - num, num - den] 

115 elif num / den < 0: 

116 num = abs(num) 

117 den = abs(den) 

118 x = [den, num, cls._np2(num + den) - num - den] 

119 elif 0 < num < den: 

120 x = [num, cls._np2(den) - den, den - num] 

121 else: 

122 assert False, "Unexpected exponent." 

123 

124 N = cls._count_number_tree_node_types(x) 

125 

126 if n == 1: 

127 yield ("var", RealVariable.make_var_type(dim=1, bnd=0), N - 1) 

128 yield ("con", RSOCConstraint.make_type(argdim=1), N) 

129 if hasM: 

130 yield ("con", 

131 AffineConstraint.make_type(dim=1, eq=False), 1) 

132 else: 

133 if complex: 

134 yield ("var", 

135 HermitianVariable.make_var_type(dim=n**2, bnd=0), N) 

136 yield ("con", 

137 ComplexLMIConstraint.make_type(diag=2*n), N) 

138 else: 

139 yield ("var", SymmetricVariable.make_var_type( 

140 dim=(n * (n + 1)) // 2, bnd=0), N) 

141 yield ("con", LMIConstraint.make_type(diag=2*n), N) 

142 yield ("con", AffineConstraint.make_type(dim=1, eq=False), 1) 

143 

144 @classmethod 

145 def convert(cls, con, options): 

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

147 from ..expressions import Constant 

148 from ..expressions.algebra import block, rsoc 

149 from ..modeling import Problem 

150 

151 x = con.power.x 

152 n = con.power.n 

153 num = con.power.num 

154 den = con.power.den 

155 rhs = con.rhs 

156 m = con.power.m 

157 

158 vtype = "hermitian" if x.complex else "symmetric" 

159 

160 P = Problem() 

161 

162 if n == 1: 

163 idt = Constant('1', 1) 

164 varcnt = 0 

165 v = [] 

166 else: 

167 idt = Constant('I', cvx.spdiag([1.] * n)) 

168 varcnt = 1 

169 v = [P.add_variable('__v[0]', (n, n), vtype)] 

170 

171 if con.relation == Constraint.LE and num > den: 

172 pown = cls._np2(num) 

173 

174 if n == 1: 

175 lis = [rhs]*den + [x]*(pown - num) + [idt]*(num - den) 

176 else: 

177 lis = [v[0]]*den + [x]*(pown - num) + [idt]*(num - den) 

178 

179 while len(lis) > 2: 

180 newlis = [] 

181 while lis: 

182 v1 = lis.pop() 

183 v2 = lis.pop() 

184 

185 if v1 is v2: 

186 newlis.append(v2) 

187 else: 

188 if n == 1: 

189 v0 = P.add_variable( 

190 '__v[' + str(varcnt) + ']', 1) 

191 P.add_constraint((v1 & v2 & v0) << rsoc()) 

192 else: 

193 v0 = P.add_variable( 

194 '__v[' + str(varcnt) + ']', (n, n), vtype) 

195 P.add_constraint( 

196 block([[v1, v0], [v0, v2]]) >> 0) 

197 

198 varcnt += 1 

199 newlis.append(v0) 

200 v.append(v0) 

201 lis = newlis 

202 

203 if n == 1: 

204 P.add_constraint((lis[0] & lis[1] & x) << rsoc()) 

205 else: 

206 P.add_constraint(block([[lis[0], x], [x, lis[1]]]) >> 0) 

207 P.add_constraint((idt | v[0]) <= rhs) 

208 elif con.relation == Constraint.LE and num <= den: 

209 num = abs(num) 

210 den = abs(den) 

211 

212 pown = cls._np2(num + den) 

213 

214 if n == 1: 

215 lis = [rhs] * den + [x] * num + [idt] * (pown - num - den) 

216 else: 

217 lis = [v[0]] * den + [x] * num + [idt] * (pown - num - den) 

218 

219 while len(lis) > 2: 

220 newlis = [] 

221 while lis: 

222 v1 = lis.pop() 

223 v2 = lis.pop() 

224 

225 if v1 is v2: 

226 newlis.append(v2) 

227 else: 

228 if n == 1: 

229 v0 = P.add_variable( 

230 '__v[' + str(varcnt) + ']', 1) 

231 P.add_constraint((v1 & v2 & v0) << rsoc()) 

232 else: 

233 v0 = P.add_variable( 

234 '__v[' + str(varcnt) + ']', (n, n), vtype) 

235 P.add_constraint( 

236 block([[v1, v0], [v0, v2]]) >> 0) 

237 

238 varcnt += 1 

239 newlis.append(v0) 

240 v.append(v0) 

241 lis = newlis 

242 

243 if n == 1: 

244 P.add_constraint((lis[0] & lis[1] & 1) << rsoc()) 

245 else: 

246 P.add_constraint(block([[lis[0], idt], [idt, lis[1]]]) >> 0) 

247 P.add_constraint((idt | v[0]) <= rhs) 

248 elif con.relation == Constraint.GE: 

249 pown = cls._np2(den) 

250 

251 if n == 1: 

252 lis = [x]*num + [rhs]*(pown - den) + [idt]*(den - num) 

253 

254 else: 

255 lis = [x]*num + [v[0]]*(pown - den) + [idt]*(den - num) 

256 

257 while len(lis) > 2: 

258 newlis = [] 

259 while lis: 

260 v1 = lis.pop() 

261 v2 = lis.pop() 

262 

263 if v1 is v2: 

264 newlis.append(v2) 

265 else: 

266 if n == 1: 

267 v0 = P.add_variable( 

268 '__v[' + str(varcnt) + ']', 1) 

269 P.add_constraint((v1 & v2 & v0) << rsoc()) 

270 else: 

271 v0 = P.add_variable( 

272 '__v[' + str(varcnt) + ']', (n, n), vtype) 

273 P.add_constraint( 

274 block([[v1, v0], [v0, v2]]) >> 0) 

275 

276 varcnt += 1 

277 newlis.append(v0) 

278 v.append(v0) 

279 lis = newlis 

280 

281 if n == 1: 

282 if m is None: 

283 P.add_constraint((lis[0] & lis[1] & rhs) << rsoc()) 

284 else: 

285 P.add_constraint((lis[0] & lis[1] & v[0]) << rsoc()) 

286 P.add_constraint((m * v[0]) > rhs) 

287 else: 

288 P.add_constraint( 

289 block([[lis[0], v[0]], [v[0], lis[1]]]) >> 0) 

290 if m is None: 

291 P.add_constraint((idt | v[0]) > rhs) 

292 else: 

293 P.add_constraint((m | v[0]) > rhs) 

294 else: 

295 assert False, "Dijkstra-IF fallthrough." 

296 

297 return P 

298 

299 def __init__(self, power, relation, rhs): 

300 """Construct a :class:`PowerTraceConstraint`. 

301 

302 :param ~picos.expressions.PowerTrace ower: 

303 Left hand side expression. 

304 :param str relation: 

305 Constraint relation symbol. 

306 :param ~picos.expressions.AffineExpression rhs: 

307 Right hand side expression. 

308 """ 

309 from ..expressions import AffineExpression, PowerTrace 

310 

311 assert isinstance(power, PowerTrace) 

312 assert relation in self.LE + self.GE 

313 assert isinstance(rhs, AffineExpression) 

314 assert len(rhs) == 1 

315 

316 p = power.p 

317 

318 assert p != 0 and p != 1, \ 

319 "The PowerTraceConstraint should not be created for p = 0 and " \ 

320 "p = 1 as there are more direct ways to represent such powers." 

321 

322 if relation == self.LE: 

323 assert p <= 0 or p >= 1, \ 

324 "Upper bounding p-th power needs p s.t. the power is convex." 

325 else: 

326 assert p >= 0 and p <= 1, \ 

327 "Lower bounding p-th power needs p s.t. the power is concave." 

328 

329 self.power = power 

330 self.relation = relation 

331 self.rhs = rhs 

332 

333 super(PowerTraceConstraint, self).__init__(power._typeStr) 

334 

335 # HACK: Support Constraint's LHS/RHS interface. 

336 # TODO: Add a unified interface for such constraints? 

337 lhs = property(lambda self: self.power) 

338 

339 def is_trace(self): 

340 """Whether the bound concerns a trace as opposed to a scalar.""" 

341 return self.power.n > 1 

342 

343 Subtype = namedtuple("Subtype", ("diag", "num", "den", "hasM", "complex")) 

344 

345 def _subtype(self): 

346 return self.Subtype(*self.power.subtype) 

347 

348 @classmethod 

349 def _cost(cls, subtype): 

350 n = subtype.diag 

351 if subtype.complex: 

352 return n**2 + 1 

353 else: 

354 return n*(n + 1)//2 + 1 

355 

356 def _expression_names(self): 

357 yield "power" 

358 yield "rhs" 

359 

360 def _str(self): 

361 if self.relation == self.LE: 

362 return glyphs.le(self.power.string, self.rhs.string) 

363 else: 

364 return glyphs.ge(self.power.string, self.rhs.string) 

365 

366 def _get_slack(self): 

367 if self.relation == self.LE: 

368 return self.rhs.safe_value - self.power.safe_value 

369 else: 

370 return self.power.safe_value - self.rhs.safe_value 

371 

372 

373# -------------------------------------- 

374__all__ = api_end(_API_START, globals())