]> gitweb.michael.orlitzky.com - sage.d.git/blob - mjo/cone/cone.py
Ensure that we generate min_rays generators; add more cone tests.
[sage.d.git] / mjo / cone / cone.py
1 # Sage doesn't load ~/.sage/init.sage during testing (sage -t), so we
2 # have to explicitly mangle our sitedir here so that "mjo.cone"
3 # resolves.
4 from os.path import abspath
5 from site import addsitedir
6 addsitedir(abspath('../../'))
7
8 from sage.all import *
9
10 # TODO: This test fails, maybe due to a bug in the existing cone code.
11 # If we request enough generators to span the space, then the returned
12 # cone should equal the ambient space::
13 #
14 # sage: K = random_cone(min_dim=5, max_dim=5, min_rays=10, max_rays=10)
15 # sage: K.lines().dimension() == K.lattice_dim()
16 # True
17
18 def random_cone(min_dim=0, max_dim=None, min_rays=0, max_rays=None):
19 r"""
20 Generate a random rational convex polyhedral cone.
21
22 Lower and upper bounds may be provided for both the dimension of the
23 ambient space and the number of generating rays of the cone. If a
24 lower bound is left unspecified, it defaults to zero. Unspecified
25 upper bounds will be chosen randomly.
26
27 The number of generating rays is naturally limited to twice the
28 dimension of the ambient space. Take for example $\mathbb{R}^{2}$.
29 You could have the generators $\left\{ \pm e_{1}, \pm e_{2}
30 \right\}$, with cardinality $4 = 2 \cdot 2$; however any other ray
31 in the space is a nonnegative linear combination of those four.
32
33 .. NOTE:
34
35 If you do not explicitly request more than ``2 * max_dim`` rays,
36 a larger number may still be randomly generated. In that case,
37 the returned cone will simply be equal to the entire space.
38
39 INPUT:
40
41 - ``min_dim`` (default: zero) -- A nonnegative integer representing the
42 minimum dimension of the ambient lattice.
43
44 - ``max_dim`` (default: random) -- A nonnegative integer representing
45 the maximum dimension of the ambient
46 lattice.
47
48 - ``min_rays`` (default: zero) -- A nonnegative integer representing the
49 minimum number of generating rays of the
50 cone.
51
52 - ``max_rays`` (default: random) -- A nonnegative integer representing the
53 maximum number of generating rays of
54 the cone.
55
56 OUTPUT:
57
58 A new, randomly generated cone.
59
60 A ``ValueError` will be thrown under the following conditions:
61
62 * Any of ``min_dim``, ``max_dim``, ``min_rays``, or ``max_rays``
63 are negative.
64
65 * ``max_dim`` is less than ``min_dim``.
66
67 * ``max_rays`` is less than ``min_rays``.
68
69 * ``min_rays`` is greater than twice ``max_dim``.
70
71 EXAMPLES:
72
73 If we set the lower/upper bounds to zero, then our result is
74 predictable::
75
76 sage: random_cone(0,0,0,0)
77 0-d cone in 0-d lattice N
78
79 We can predict the dimension when ``min_dim == max_dim``::
80
81 sage: random_cone(min_dim=4, max_dim=4, min_rays=0, max_rays=0)
82 0-d cone in 4-d lattice N
83
84 Likewise for the number of rays when ``min_rays == max_rays``::
85
86 sage: random_cone(min_dim=10, max_dim=10, min_rays=10, max_rays=10)
87 10-d cone in 10-d lattice N
88
89 TESTS:
90
91 It's hard to test the output of a random process, but we can at
92 least make sure that we get a cone back::
93
94 sage: from sage.geometry.cone import is_Cone # long time
95 sage: K = random_cone() # long time
96 sage: is_Cone(K) # long time
97 True
98
99 The upper/lower bounds are respected::
100
101 sage: K = random_cone(min_dim=5, max_dim=10, min_rays=3, max_rays=4)
102 sage: 5 <= K.lattice_dim() and K.lattice_dim() <= 10
103 True
104 sage: 3 <= K.nrays() and K.nrays() <= 4
105 True
106
107 Ensure that an exception is raised when either lower bound is greater
108 than its respective upper bound::
109
110 sage: random_cone(min_dim=5, max_dim=2)
111 Traceback (most recent call last):
112 ...
113 ValueError: max_dim cannot be less than min_dim.
114
115 sage: random_cone(min_rays=5, max_rays=2)
116 Traceback (most recent call last):
117 ...
118 ValueError: max_rays cannot be less than min_rays.
119
120 And if we request too many rays::
121
122 sage: random_cone(min_rays=5, max_dim=1)
123 Traceback (most recent call last):
124 ...
125 ValueError: min_rays cannot be larger than twice max_dim.
126
127 """
128
129 # Catch obvious mistakes so that we can generate clear error
130 # messages.
131
132 if min_dim < 0:
133 raise ValueError('min_dim must be nonnegative.')
134
135 if min_rays < 0:
136 raise ValueError('min_rays must be nonnegative.')
137
138 if max_dim is not None:
139 if max_dim < 0:
140 raise ValueError('max_dim must be nonnegative.')
141 if (max_dim < min_dim):
142 raise ValueError('max_dim cannot be less than min_dim.')
143 if min_rays > 2*max_dim:
144 raise ValueError('min_rays cannot be larger than twice max_dim.')
145
146 if max_rays is not None:
147 if max_rays < 0:
148 raise ValueError('max_rays must be nonnegative.')
149 if (max_rays < min_rays):
150 raise ValueError('max_rays cannot be less than min_rays.')
151
152
153 def random_min_max(l,u):
154 r"""
155 We need to handle two cases for the upper bounds, and we need to do
156 the same thing for max_dim/max_rays. So we consolidate the logic here.
157 """
158 if u is None:
159 # The upper bound is unspecified; return a random integer
160 # in [l,infinity).
161 return l + ZZ.random_element().abs()
162 else:
163 # We have an upper bound, and it's greater than or equal
164 # to our lower bound. So we generate a random integer in
165 # [0,u-l], and then add it to l to get something in
166 # [l,u]. To understand the "+1", check the
167 # ZZ.random_element() docs.
168 return l + ZZ.random_element(u - l + 1)
169
170
171 d = random_min_max(min_dim, max_dim)
172 r = random_min_max(min_rays, max_rays)
173
174 L = ToricLattice(d)
175
176 # The rays are trickier to generate, since we could generate v and
177 # 2*v as our "two rays." In that case, the resuting cone would
178 # have one generating ray. To avoid such a situation, we start by
179 # generating ``r`` rays where ``r`` is the number we want to end
180 # up with.
181 #
182 # However, since we're going to *check* whether or not we actually
183 # have ``r``, we need ``r`` rays to be attainable. So we need to
184 # limit ``r`` to twice the dimension of the ambient space.
185 #
186 r = min(r, 2*d)
187 rays = [L.random_element() for i in range(0, r)]
188
189 # (The lattice parameter is required when no rays are given, so we
190 # pass it just in case ``r == 0``).
191 K = Cone(rays, lattice=L)
192
193 # Now if we generated two of the "same" rays, we'll have fewer
194 # generating rays than ``r``. In that case, we keep making up new
195 # rays and recreating the cone until we get the right number of
196 # independent generators.
197 while r > K.nrays():
198 rays.append(L.random_element())
199 K = Cone(rays)
200
201 return K
202
203
204 def discrete_complementarity_set(K):
205 r"""
206 Compute the discrete complementarity set of this cone.
207
208 The complementarity set of this cone is the set of all orthogonal
209 pairs `(x,s)` such that `x` is in this cone, and `s` is in its
210 dual. The discrete complementarity set restricts `x` and `s` to be
211 generators of their respective cones.
212
213 OUTPUT:
214
215 A list of pairs `(x,s)` such that,
216
217 * `x` is in this cone.
218 * `x` is a generator of this cone.
219 * `s` is in this cone's dual.
220 * `s` is a generator of this cone's dual.
221 * `x` and `s` are orthogonal.
222
223 EXAMPLES:
224
225 The discrete complementarity set of the nonnegative orthant consists
226 of pairs of standard basis vectors::
227
228 sage: K = Cone([(1,0),(0,1)])
229 sage: discrete_complementarity_set(K)
230 [((1, 0), (0, 1)), ((0, 1), (1, 0))]
231
232 If the cone consists of a single ray, the second components of the
233 discrete complementarity set should generate the orthogonal
234 complement of that ray::
235
236 sage: K = Cone([(1,0)])
237 sage: discrete_complementarity_set(K)
238 [((1, 0), (0, 1)), ((1, 0), (0, -1))]
239 sage: K = Cone([(1,0,0)])
240 sage: discrete_complementarity_set(K)
241 [((1, 0, 0), (0, 1, 0)),
242 ((1, 0, 0), (0, -1, 0)),
243 ((1, 0, 0), (0, 0, 1)),
244 ((1, 0, 0), (0, 0, -1))]
245
246 When the cone is the entire space, its dual is the trivial cone, so
247 the discrete complementarity set is empty::
248
249 sage: K = Cone([(1,0),(-1,0),(0,1),(0,-1)])
250 sage: discrete_complementarity_set(K)
251 []
252
253 TESTS:
254
255 The complementarity set of the dual can be obtained by switching the
256 components of the complementarity set of the original cone::
257
258 sage: K1 = random_cone(max_dim=10, max_rays=10)
259 sage: K2 = K1.dual()
260 sage: expected = [(x,s) for (s,x) in discrete_complementarity_set(K2)]
261 sage: actual = discrete_complementarity_set(K1)
262 sage: actual == expected
263 True
264
265 """
266 V = K.lattice().vector_space()
267
268 # Convert the rays to vectors so that we can compute inner
269 # products.
270 xs = [V(x) for x in K.rays()]
271 ss = [V(s) for s in K.dual().rays()]
272
273 return [(x,s) for x in xs for s in ss if x.inner_product(s) == 0]
274
275
276 def lyapunov_rank(K):
277 r"""
278 Compute the Lyapunov (or bilinearity) rank of this cone.
279
280 The Lyapunov rank of a cone can be thought of in (mainly) two ways:
281
282 1. The dimension of the Lie algebra of the automorphism group of the
283 cone.
284
285 2. The dimension of the linear space of all Lyapunov-like
286 transformations on the cone.
287
288 INPUT:
289
290 A closed, convex polyhedral cone.
291
292 OUTPUT:
293
294 An integer representing the Lyapunov rank of the cone. If the
295 dimension of the ambient vector space is `n`, then the Lyapunov rank
296 will be between `1` and `n` inclusive; however a rank of `n-1` is
297 not possible (see the first reference).
298
299 .. note::
300
301 In the references, the cones are always assumed to be proper. We
302 do not impose this restriction.
303
304 .. seealso::
305
306 :meth:`is_proper`
307
308 ALGORITHM:
309
310 The codimension formula from the second reference is used. We find
311 all pairs `(x,s)` in the complementarity set of `K` such that `x`
312 and `s` are rays of our cone. It is known that these vectors are
313 sufficient to apply the codimension formula. Once we have all such
314 pairs, we "brute force" the codimension formula by finding all
315 linearly-independent `xs^{T}`.
316
317 REFERENCES:
318
319 1. M.S. Gowda and J. Tao. On the bilinearity rank of a proper cone
320 and Lyapunov-like transformations, Mathematical Programming, 147
321 (2014) 155-170.
322
323 2. G. Rudolf, N. Noyan, D. Papp, and F. Alizadeh, Bilinear
324 optimality constraints for the cone of positive polynomials,
325 Mathematical Programming, Series B, 129 (2011) 5-31.
326
327 EXAMPLES:
328
329 The nonnegative orthant in `\mathbb{R}^{n}` always has rank `n`::
330
331 sage: positives = Cone([(1,)])
332 sage: lyapunov_rank(positives)
333 1
334 sage: quadrant = Cone([(1,0), (0,1)])
335 sage: lyapunov_rank(quadrant)
336 2
337 sage: octant = Cone([(1,0,0), (0,1,0), (0,0,1)])
338 sage: lyapunov_rank(octant)
339 3
340
341 The `L^{3}_{1}` cone is known to have a Lyapunov rank of one::
342
343 sage: L31 = Cone([(1,0,1), (0,-1,1), (-1,0,1), (0,1,1)])
344 sage: lyapunov_rank(L31)
345 1
346
347 Likewise for the `L^{3}_{\infty}` cone::
348
349 sage: L3infty = Cone([(0,1,1), (1,0,1), (0,-1,1), (-1,0,1)])
350 sage: lyapunov_rank(L3infty)
351 1
352
353 The Lyapunov rank should be additive on a product of cones::
354
355 sage: L31 = Cone([(1,0,1), (0,-1,1), (-1,0,1), (0,1,1)])
356 sage: octant = Cone([(1,0,0), (0,1,0), (0,0,1)])
357 sage: K = L31.cartesian_product(octant)
358 sage: lyapunov_rank(K) == lyapunov_rank(L31) + lyapunov_rank(octant)
359 True
360
361 Two isomorphic cones should have the same Lyapunov rank. The cone
362 ``K`` in the following example is isomorphic to the nonnegative
363 octant in `\mathbb{R}^{3}`::
364
365 sage: K = Cone([(1,2,3), (-1,1,0), (1,0,6)])
366 sage: lyapunov_rank(K)
367 3
368
369 The dual cone `K^{*}` of ``K`` should have the same Lyapunov rank as ``K``
370 itself::
371
372 sage: K = Cone([(2,2,4), (-1,9,0), (2,0,6)])
373 sage: lyapunov_rank(K) == lyapunov_rank(K.dual())
374 True
375
376 TESTS:
377
378 The Lyapunov rank should be additive on a product of cones::
379
380 sage: K1 = random_cone(max_dim=10, max_rays=10)
381 sage: K2 = random_cone(max_dim=10, max_rays=10)
382 sage: K = K1.cartesian_product(K2)
383 sage: lyapunov_rank(K) == lyapunov_rank(K1) + lyapunov_rank(K2)
384 True
385
386 The dual cone `K^{*}` of ``K`` should have the same Lyapunov rank as ``K``
387 itself::
388
389 sage: K = random_cone(max_dim=10, max_rays=10)
390 sage: lyapunov_rank(K) == lyapunov_rank(K.dual())
391 True
392
393 """
394 V = K.lattice().vector_space()
395
396 C_of_K = discrete_complementarity_set(K)
397
398 matrices = [x.tensor_product(s) for (x,s) in C_of_K]
399
400 # Sage doesn't think matrices are vectors, so we have to convert
401 # our matrices to vectors explicitly before we can figure out how
402 # many are linearly-indepenedent.
403 #
404 # The space W has the same base ring as V, but dimension
405 # dim(V)^2. So it has the same dimension as the space of linear
406 # transformations on V. In other words, it's just the right size
407 # to create an isomorphism between it and our matrices.
408 W = VectorSpace(V.base_ring(), V.dimension()**2)
409
410 def phi(m):
411 r"""
412 Convert a matrix to a vector isomorphically.
413 """
414 return W(m.list())
415
416 vectors = [phi(m) for m in matrices]
417
418 return (W.dimension() - W.span(vectors).rank())