-# 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 is_lyapunov_like(L,K):
The identity is always Lyapunov-like in a nontrivial space::
sage: set_random_seed()
- sage: K = random_cone(min_ambient_dim = 1, max_rays = 8)
+ sage: K = random_cone(min_ambient_dim=1, max_ambient_dim=8)
sage: L = identity_matrix(K.lattice_dim())
sage: is_lyapunov_like(L,K)
True
As is the "zero" transformation::
- sage: K = random_cone(min_ambient_dim = 1, max_rays = 5)
+ sage: K = random_cone(min_ambient_dim=1, max_ambient_dim=8)
sage: R = K.lattice().vector_space().base_ring()
sage: L = zero_matrix(R, K.lattice_dim())
sage: is_lyapunov_like(L,K)
Everything in ``K.lyapunov_like_basis()`` should be Lyapunov-like
on ``K``::
- sage: K = random_cone(min_ambient_dim = 1, max_rays = 5)
+ sage: K = random_cone(min_ambient_dim=1, max_ambient_dim=6)
sage: all([ is_lyapunov_like(L,K) for L in K.lyapunov_like_basis() ])
True
for (x,s) in K.discrete_complementarity_set()])
-def random_element(K):
+def motzkin_decomposition(K):
r"""
- Return a random element of ``K`` from its ambient vector space.
+ Return the pair of components in the Motzkin decomposition of this cone.
+
+ Every convex cone is the direct sum of a strictly convex cone and a
+ linear subspace [Stoer-Witzgall]_. Return a pair ``(P,S)`` of cones
+ such that ``P`` is strictly convex, ``S`` is a subspace, and ``K``
+ is the direct sum of ``P`` and ``S``.
- ALGORITHM:
+ OUTPUT:
- The cone ``K`` is specified in terms of its generators, so that
- ``K`` is equal to the convex conic combination of those generators.
- To choose a random element of ``K``, we assign random nonnegative
- coefficients to each generator of ``K`` and construct a new vector
- from the scaled rays.
+ An ordered pair ``(P,S)`` of closed convex polyhedral cones where
+ ``P`` is strictly convex, ``S`` is a subspace, and ``K`` is the
+ direct sum of ``P`` and ``S``.
- A vector, rather than a ray, is returned so that the element may
- have non-integer coordinates. Thus the element may have an
- arbitrarily small norm.
+ REFERENCES:
+
+ .. [Stoer-Witzgall] J. Stoer and C. Witzgall. Convexity and
+ Optimization in Finite Dimensions I. Springer-Verlag, New
+ York, 1970.
EXAMPLES:
- A random element of the trivial cone is zero::
+ The nonnegative orthant is strictly convex, so it is its own
+ strictly convex component and its subspace component is trivial::
- sage: set_random_seed()
- sage: K = Cone([], ToricLattice(0))
- sage: random_element(K)
- ()
- sage: K = Cone([(0,)])
- sage: random_element(K)
- (0)
- sage: K = Cone([(0,0)])
- sage: random_element(K)
- (0, 0)
- sage: K = Cone([(0,0,0)])
- sage: random_element(K)
- (0, 0, 0)
+ sage: K = Cone([(1,0,0),(0,1,0),(0,0,1)])
+ sage: (P,S) = motzkin_decomposition(K)
+ sage: K.is_equivalent(P)
+ True
+ sage: S.is_trivial()
+ True
+
+ Likewise, full spaces are their own subspace components::
+
+ sage: K = Cone([(1,0),(-1,0),(0,1),(0,-1)])
+ sage: K.is_full_space()
+ True
+ sage: (P,S) = motzkin_decomposition(K)
+ sage: K.is_equivalent(S)
+ True
+ sage: P.is_trivial()
+ True
TESTS:
- Any cone should contain an element of itself::
+ A random point in the cone should belong to either the strictly
+ convex component or the subspace component. If the point is nonzero,
+ it cannot be in both::
sage: set_random_seed()
- sage: K = random_cone(max_rays = 8)
- sage: K.contains(random_element(K))
+ sage: K = random_cone(max_ambient_dim=8)
+ sage: (P,S) = motzkin_decomposition(K)
+ sage: x = K.random_element()
+ sage: P.contains(x) or S.contains(x)
True
+ sage: x.is_zero() or (P.contains(x) != S.contains(x))
+ True
+
+ The strictly convex component should always be strictly convex, and
+ the subspace component should always be a subspace::
+ sage: set_random_seed()
+ sage: K = random_cone(max_ambient_dim=8)
+ sage: (P,S) = motzkin_decomposition(K)
+ sage: P.is_strictly_convex()
+ True
+ sage: S.lineality() == S.dim()
+ True
+
+ The generators of the components are obtained from orthogonal
+ projections of the original generators [Stoer-Witzgall]_::
+
+ sage: set_random_seed()
+ sage: K = random_cone(max_ambient_dim=8)
+ sage: (P,S) = motzkin_decomposition(K)
+ sage: A = S.linear_subspace().complement().matrix()
+ sage: proj_S_perp = A.transpose() * (A*A.transpose()).inverse() * A
+ sage: expected_P = Cone([ proj_S_perp*g for g in K ], K.lattice())
+ sage: P.is_equivalent(expected_P)
+ True
+ sage: A = S.linear_subspace().matrix()
+ sage: proj_S = A.transpose() * (A*A.transpose()).inverse() * A
+ sage: expected_S = Cone([ proj_S*g for g in K ], K.lattice())
+ sage: S.is_equivalent(expected_S)
+ True
"""
- V = K.lattice().vector_space()
- F = V.base_ring()
- coefficients = [ F.random_element().abs() for i in range(K.nrays()) ]
- vector_gens = map(V, K.rays())
- scaled_gens = [ coefficients[i]*vector_gens[i]
- for i in range(len(vector_gens)) ]
+ linspace_gens = [ copy(b) for b in K.linear_subspace().basis() ]
+ linspace_gens += [ -b for b in linspace_gens ]
+
+ S = Cone(linspace_gens, K.lattice())
- # Make sure we return a vector. Without the coercion, we might
- # return ``0`` when ``K`` has no rays.
- v = V(sum(scaled_gens))
- return v
+ # Since ``S`` is a subspace, its dual is its orthogonal complement
+ # (albeit in the wrong lattice).
+ S_perp = Cone(S.dual(), K.lattice())
+ P = K.intersection(S_perp)
+ return (P,S)
-def positive_operators(K):
+def positive_operator_gens(K):
r"""
Compute generators of the cone of positive operators on this cone.
The trivial cone in a trivial space has no positive operators::
sage: K = Cone([], ToricLattice(0))
- sage: positive_operators(K)
+ sage: positive_operator_gens(K)
[]
Positive operators on the nonnegative orthant are nonnegative matrices::
sage: K = Cone([(1,)])
- sage: positive_operators(K)
+ sage: positive_operator_gens(K)
[[1]]
sage: K = Cone([(1,0),(0,1)])
- sage: positive_operators(K)
+ sage: positive_operator_gens(K)
[
[1 0] [0 1] [0 0] [0 0]
[0 0], [0 0], [1 0], [0 1]
sage: K = Cone([(1,),(-1,)])
sage: K.is_full_space()
True
- sage: positive_operators(K)
+ sage: positive_operator_gens(K)
[[1], [-1]]
sage: K = Cone([(1,0),(-1,0),(0,1),(0,-1)])
sage: K.is_full_space()
True
- sage: positive_operators(K)
+ sage: positive_operator_gens(K)
[
[1 0] [-1 0] [0 1] [ 0 -1] [0 0] [ 0 0] [0 0] [ 0 0]
[0 0], [ 0 0], [0 0], [ 0 0], [1 0], [-1 0], [0 1], [ 0 -1]
A positive operator on a cone should send its generators into the cone::
- sage: K = random_cone(max_ambient_dim = 6)
- sage: pi_of_K = positive_operators(K)
+ sage: set_random_seed()
+ sage: K = random_cone(max_ambient_dim=5)
+ sage: pi_of_K = positive_operator_gens(K)
sage: all([K.contains(p*x) for p in pi_of_K for x in K.rays()])
True
+ The dimension of the cone of positive operators is given by the
+ corollary in my paper::
+
+ sage: set_random_seed()
+ sage: K = random_cone(max_ambient_dim=5)
+ sage: n = K.lattice_dim()
+ sage: m = K.dim()
+ sage: l = K.lineality()
+ sage: pi_of_K = positive_operator_gens(K)
+ sage: L = ToricLattice(n**2)
+ sage: actual = Cone([p.list() for p in pi_of_K], lattice=L).dim()
+ sage: expected = n**2 - l*(m - l) - (n - m)*m
+ sage: actual == expected
+ True
+
+ The lineality of the cone of positive operators is given by the
+ corollary in my paper::
+
+ sage: set_random_seed()
+ sage: K = random_cone(max_ambient_dim=5)
+ sage: n = K.lattice_dim()
+ sage: pi_of_K = positive_operator_gens(K)
+ sage: L = ToricLattice(n**2)
+ sage: actual = Cone([p.list() for p in pi_of_K], lattice=L).lineality()
+ sage: expected = n**2 - K.dim()*K.dual().dim()
+ sage: actual == expected
+ True
+
+ The cone ``K`` is proper if and only if the cone of positive
+ operators on ``K`` is proper::
+
+ sage: set_random_seed()
+ sage: K = random_cone(max_ambient_dim=5)
+ sage: pi_of_K = positive_operator_gens(K)
+ sage: L = ToricLattice(K.lattice_dim()**2)
+ sage: pi_cone = Cone([p.list() for p in pi_of_K], lattice=L)
+ sage: K.is_proper() == pi_cone.is_proper()
+ True
"""
- # 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.
- V = K.lattice().vector_space()
- W = VectorSpace(V.base_ring(), V.dimension()**2)
+ # Matrices are not vectors in Sage, so we have to convert them
+ # to vectors explicitly before we can find a basis. We need these
+ # two values to construct the appropriate "long vector" space.
+ F = K.lattice().base_field()
+ n = K.lattice_dim()
tensor_products = [ s.tensor_product(x) for x in K for s in K.dual() ]
- # Turn our matrices into long vectors...
- vectors = [ W(m.list()) for m in tensor_products ]
+ # Convert those tensor products to long vectors.
+ W = VectorSpace(F, n**2)
+ vectors = [ W(tp.list()) for tp in tensor_products ]
# Create the *dual* cone of the positive operators, expressed as
# long vectors..
- L = ToricLattice(W.dimension())
- pi_dual = Cone(vectors, lattice=L)
+ pi_dual = Cone(vectors, ToricLattice(W.dimension()))
# Now compute the desired cone from its dual...
pi_cone = pi_dual.dual()
# And finally convert its rays back to matrix representations.
- M = MatrixSpace(V.base_ring(), V.dimension())
-
+ M = MatrixSpace(F, n)
return [ M(v.list()) for v in pi_cone.rays() ]
-def Z_transformations(K):
+def Z_transformation_gens(K):
r"""
Compute generators of the cone of Z-transformations on this cone.
That is, matrices whose off-diagonal elements are nonnegative::
sage: K = Cone([(1,0),(0,1)])
- sage: Z_transformations(K)
+ sage: Z_transformation_gens(K)
[
[ 0 -1] [ 0 0] [-1 0] [1 0] [ 0 0] [0 0]
[ 0 0], [-1 0], [ 0 0], [0 0], [ 0 -1], [0 1]
]
sage: K = Cone([(1,0,0,0),(0,1,0,0),(0,0,1,0),(0,0,0,1)])
- sage: all([ z[i][j] <= 0 for z in Z_transformations(K)
+ sage: all([ z[i][j] <= 0 for z in Z_transformation_gens(K)
....: for i in range(z.nrows())
....: for j in range(z.ncols())
....: if i != j ])
The trivial cone in a trivial space has no Z-transformations::
sage: K = Cone([], ToricLattice(0))
- sage: Z_transformations(K)
+ sage: Z_transformation_gens(K)
[]
Z-transformations on a subspace are Lyapunov-like and vice-versa::
sage: K.is_full_space()
True
sage: lls = span([ vector(l.list()) for l in K.lyapunov_like_basis() ])
- sage: zs = span([ vector(z.list()) for z in Z_transformations(K) ])
+ sage: zs = span([ vector(z.list()) for z in Z_transformation_gens(K) ])
sage: zs == lls
True
The Z-property is possessed by every Z-transformation::
sage: set_random_seed()
- sage: K = random_cone(max_ambient_dim = 6)
- sage: Z_of_K = Z_transformations(K)
+ sage: K = random_cone(max_ambient_dim=6)
+ sage: Z_of_K = Z_transformation_gens(K)
sage: dcs = K.discrete_complementarity_set()
sage: all([(z*x).inner_product(s) <= 0 for z in Z_of_K
....: for (x,s) in dcs])
The lineality space of Z is LL::
sage: set_random_seed()
- sage: K = random_cone(min_ambient_dim = 1, max_ambient_dim = 6)
+ sage: K = random_cone(min_ambient_dim=1, max_ambient_dim=6)
sage: lls = span([ vector(l.list()) for l in K.lyapunov_like_basis() ])
- sage: z_cone = Cone([ z.list() for z in Z_transformations(K) ])
+ sage: z_cone = Cone([ z.list() for z in Z_transformation_gens(K) ])
sage: z_cone.linear_subspace() == lls
True
+ And thus, the lineality of Z is the Lyapunov rank::
+
+ sage: set_random_seed()
+ sage: K = random_cone(max_ambient_dim=6)
+ sage: Z_of_K = Z_transformation_gens(K)
+ sage: L = ToricLattice(K.lattice_dim()**2)
+ sage: z_cone = Cone([ z.list() for z in Z_of_K ], lattice=L)
+ sage: z_cone.lineality() == K.lyapunov_rank()
+ True
+
+ The lineality spaces of pi-star and Z-star are equal:
+
+ sage: set_random_seed()
+ sage: K = random_cone(max_ambient_dim=5)
+ sage: pi_of_K = positive_operator_gens(K)
+ sage: Z_of_K = Z_transformation_gens(K)
+ sage: L = ToricLattice(K.lattice_dim()**2)
+ sage: pi_star = Cone([p.list() for p in pi_of_K], lattice=L).dual()
+ sage: z_star = Cone([ z.list() for z in Z_of_K], lattice=L).dual()
+ sage: pi_star.linear_subspace() == z_star.linear_subspace()
+ True
"""
- # 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.
- V = K.lattice().vector_space()
- W = VectorSpace(V.base_ring(), V.dimension()**2)
-
- C_of_K = K.discrete_complementarity_set()
- tensor_products = [ s.tensor_product(x) for (x,s) in C_of_K ]
+ # Matrices are not vectors in Sage, so we have to convert them
+ # to vectors explicitly before we can find a basis. We need these
+ # two values to construct the appropriate "long vector" space.
+ F = K.lattice().base_field()
+ n = K.lattice_dim()
+
+ # These tensor products contain generators for the dual cone of
+ # the cross-positive transformations.
+ tensor_products = [ s.tensor_product(x)
+ for (x,s) in K.discrete_complementarity_set() ]
# Turn our matrices into long vectors...
+ W = VectorSpace(F, n**2)
vectors = [ W(m.list()) for m in tensor_products ]
# Create the *dual* cone of the cross-positive operators,
# expressed as long vectors..
- L = ToricLattice(W.dimension())
- Sigma_dual = Cone(vectors, lattice=L)
+ Sigma_dual = Cone(vectors, lattice=ToricLattice(W.dimension()))
# Now compute the desired cone from its dual...
Sigma_cone = Sigma_dual.dual()
# And finally convert its rays back to matrix representations.
# But first, make them negative, so we get Z-transformations and
# not cross-positive ones.
- M = MatrixSpace(V.base_ring(), V.dimension())
-
+ M = MatrixSpace(F, n)
return [ -M(v.list()) for v in Sigma_cone.rays() ]
+
+
+def Z_cone(K):
+ gens = Z_transformation_gens(K)
+ L = None
+ if len(gens) == 0:
+ L = ToricLattice(0)
+ return Cone([ g.list() for g in gens ], lattice=L)
+
+def pi_cone(K):
+ gens = positive_operator_gens(K)
+ L = None
+ if len(gens) == 0:
+ L = ToricLattice(0)
+ return Cone([ g.list() for g in gens ], lattice=L)