# Sage doesn't load ~/.sage/init.sage during testing (sage -t), so we # have to explicitly mangle our sitedir here so that "mjo.cone" # resolves. from os.path import abspath from site import addsitedir addsitedir(abspath('../../')) from sage.all import * def random_cone(min_dim=None, max_dim=None, min_rays=None, max_rays=None): r""" Generate a random rational convex polyhedral cone. Lower and upper bounds may be provided for both the dimension of the ambient space and the number of generating rays of the cone. Any parameters left unspecified will be chosen randomly. INPUT: - ``min_dim`` (default: random) -- The minimum dimension of the ambient lattice. - ``max_dim`` (default: random) -- The maximum dimension of the ambient lattice. - ``min_rays`` (default: random) -- The minimum number of generating rays of the cone. - ``max_rays`` (default: random) -- The maximum number of generating rays of the cone. OUTPUT: A new, randomly generated cone. TESTS: It's hard to test the output of a random process, but we can at least make sure that we get a cone back:: sage: from sage.geometry.cone import is_Cone sage: K = random_cone() sage: is_Cone(K) # long time True """ def random_min_max(l,u): r""" We need to handle four cases to prevent us from doing something stupid like having an upper bound that's lower than our lower bound. And we would need to repeat all of that logic for the dimension/rays, so we consolidate it here. """ if l is None and u is None: # They're both random, just return a random nonnegative # integer. return ZZ.random_element().abs() if l is not None and u is not None: # Both were specified. Again, just make up a number and # return it. If the user wants to give us u < l then he # can have an exception. return ZZ.random_element(l,u) if l is not None and u is None: # In this case, we're generating the upper bound randomly # GIVEN A LOWER BOUND. So we add a random nonnegative # integer to the given lower bound. u = l + ZZ.random_element().abs() return ZZ.random_element(l,u) # Here we must be in the only remaining case, where we are # given an upper bound but no lower bound. We might as well # use zero. return ZZ.random_element(0,u) d = random_min_max(min_dim, max_dim) r = random_min_max(min_rays, max_rays) L = ToricLattice(d) rays = [L.random_element() for i in range(0,r)] # We pass the lattice in case there are no rays. return Cone(rays, lattice=L) def discrete_complementarity_set(K): r""" Compute the discrete complementarity set of this cone. The complementarity set of this cone is the set of all orthogonal pairs `(x,s)` such that `x` is in this cone, and `s` is in its dual. The discrete complementarity set restricts `x` and `s` to be generators of their respective cones. OUTPUT: A list of pairs `(x,s)` such that, * `x` is in this cone. * `x` is a generator of this cone. * `s` is in this cone's dual. * `s` is a generator of this cone's dual. * `x` and `s` are orthogonal. EXAMPLES: The discrete complementarity set of the nonnegative orthant consists of pairs of standard basis vectors:: sage: K = Cone([(1,0),(0,1)]) sage: discrete_complementarity_set(K) [((1, 0), (0, 1)), ((0, 1), (1, 0))] If the cone consists of a single ray, the second components of the discrete complementarity set should generate the orthogonal complement of that ray:: sage: K = Cone([(1,0)]) sage: discrete_complementarity_set(K) [((1, 0), (0, 1)), ((1, 0), (0, -1))] sage: K = Cone([(1,0,0)]) sage: discrete_complementarity_set(K) [((1, 0, 0), (0, 1, 0)), ((1, 0, 0), (0, -1, 0)), ((1, 0, 0), (0, 0, 1)), ((1, 0, 0), (0, 0, -1))] When the cone is the entire space, its dual is the trivial cone, so the discrete complementarity set is empty:: sage: K = Cone([(1,0),(-1,0),(0,1),(0,-1)]) sage: discrete_complementarity_set(K) [] TESTS: The complementarity set of the dual can be obtained by switching the components of the complementarity set of the original cone:: sage: K1 = random_cone(0,10,0,10) sage: K2 = K1.dual() sage: expected = [(x,s) for (s,x) in discrete_complementarity_set(K2)] sage: actual = discrete_complementarity_set(K1) sage: actual == expected True """ V = K.lattice().vector_space() # Convert the rays to vectors so that we can compute inner # products. xs = [V(x) for x in K.rays()] ss = [V(s) for s in K.dual().rays()] return [(x,s) for x in xs for s in ss if x.inner_product(s) == 0] def lyapunov_rank(K): r""" Compute the Lyapunov (or bilinearity) rank of this cone. The Lyapunov rank of a cone can be thought of in (mainly) two ways: 1. The dimension of the Lie algebra of the automorphism group of the cone. 2. The dimension of the linear space of all Lyapunov-like transformations on the cone. INPUT: A closed, convex polyhedral cone. OUTPUT: An integer representing the Lyapunov rank of the cone. If the dimension of the ambient vector space is `n`, then the Lyapunov rank will be between `1` and `n` inclusive; however a rank of `n-1` is not possible (see the first reference). .. note:: In the references, the cones are always assumed to be proper. We do not impose this restriction. .. seealso:: :meth:`is_proper` ALGORITHM: The codimension formula from the second reference is used. We find all pairs `(x,s)` in the complementarity set of `K` such that `x` and `s` are rays of our cone. It is known that these vectors are sufficient to apply the codimension formula. Once we have all such pairs, we "brute force" the codimension formula by finding all linearly-independent `xs^{T}`. REFERENCES: 1. M.S. Gowda and J. Tao. On the bilinearity rank of a proper cone and Lyapunov-like transformations, Mathematical Programming, 147 (2014) 155-170. 2. G. Rudolf, N. Noyan, D. Papp, and F. Alizadeh, Bilinear optimality constraints for the cone of positive polynomials, Mathematical Programming, Series B, 129 (2011) 5-31. EXAMPLES: The nonnegative orthant in `\mathbb{R}^{n}` always has rank `n`:: sage: positives = Cone([(1,)]) sage: lyapunov_rank(positives) 1 sage: quadrant = Cone([(1,0), (0,1)]) sage: lyapunov_rank(quadrant) 2 sage: octant = Cone([(1,0,0), (0,1,0), (0,0,1)]) sage: lyapunov_rank(octant) 3 The `L^{3}_{1}` cone is known to have a Lyapunov rank of one:: sage: L31 = Cone([(1,0,1), (0,-1,1), (-1,0,1), (0,1,1)]) sage: lyapunov_rank(L31) 1 Likewise for the `L^{3}_{\infty}` cone:: sage: L3infty = Cone([(0,1,1), (1,0,1), (0,-1,1), (-1,0,1)]) sage: lyapunov_rank(L3infty) 1 The Lyapunov rank should be additive on a product of cones:: sage: L31 = Cone([(1,0,1), (0,-1,1), (-1,0,1), (0,1,1)]) sage: octant = Cone([(1,0,0), (0,1,0), (0,0,1)]) sage: K = L31.cartesian_product(octant) sage: lyapunov_rank(K) == lyapunov_rank(L31) + lyapunov_rank(octant) True Two isomorphic cones should have the same Lyapunov rank. The cone ``K`` in the following example is isomorphic to the nonnegative octant in `\mathbb{R}^{3}`:: sage: K = Cone([(1,2,3), (-1,1,0), (1,0,6)]) sage: lyapunov_rank(K) 3 The dual cone `K^{*}` of ``K`` should have the same Lyapunov rank as ``K`` itself:: sage: K = Cone([(2,2,4), (-1,9,0), (2,0,6)]) sage: lyapunov_rank(K) == lyapunov_rank(K.dual()) True TESTS: The Lyapunov rank should be additive on a product of cones:: sage: K1 = random_cone(0,10,0,10) sage: K2 = random_cone(0,10,0,10) sage: K = K1.cartesian_product(K2) sage: lyapunov_rank(K) == lyapunov_rank(K1) + lyapunov_rank(K2) True The dual cone `K^{*}` of ``K`` should have the same Lyapunov rank as ``K`` itself:: sage: K = random_cone(0,10,0,10) sage: lyapunov_rank(K) == lyapunov_rank(K.dual()) True """ V = K.lattice().vector_space() C_of_K = discrete_complementarity_set(K) matrices = [x.tensor_product(s) for (x,s) in C_of_K] # Sage doesn't think matrices are vectors, so we have to convert # our matrices to vectors explicitly before we can figure out how # many are linearly-indepenedent. # # The space W has the same base ring as V, but dimension # dim(V)^2. So it has the same dimension as the space of linear # transformations on V. In other words, it's just the right size # to create an isomorphism between it and our matrices. W = VectorSpace(V.base_ring(), V.dimension()**2) def phi(m): r""" Convert a matrix to a vector isomorphically. """ return W(m.list()) vectors = [phi(m) for m in matrices] return (W.dimension() - W.span(vectors).rank())