X-Git-Url: http://gitweb.michael.orlitzky.com/?a=blobdiff_plain;f=mjo%2Fcone%2Fcone.py;h=e9d0f1e643e3a0d40d8a754faf551ac317e36f37;hb=d27a3f72ac6b898534615de9111ace4082a4c55e;hp=6ade5e628f1035c99294048c7fb55b4b9c1204d9;hpb=7d2f3fba7f494158dbce5f7a3eca1d15ee7f577e;p=sage.d.git diff --git a/mjo/cone/cone.py b/mjo/cone/cone.py index 6ade5e6..e9d0f1e 100644 --- a/mjo/cone/cone.py +++ b/mjo/cone/cone.py @@ -8,124 +8,231 @@ addsitedir(abspath('../../')) from sage.all import * -def random_cone(min_dim=0, max_dim=None, min_rays=0, max_rays=None): +def basically_the_same(K1,K2): r""" - Generate a random rational convex polyhedral cone. + ``True`` if ``K1`` and ``K2`` are basically the same, and ``False`` + otherwise. This is intended as a lazy way to check whether or not + ``K1`` and ``K2`` are linearly isomorphic (i.e. ``A(K1) == K2`` for + some invertible linear transformation ``A``). + """ + if K1.lattice_dim() != K2.lattice_dim(): + return False - Lower and upper bounds may be provided for both the dimension of the - ambient space and the number of generating rays of the cone. If a - lower bound is left unspecified, it defaults to zero. Unspecified - upper bounds will be chosen randomly. + if K1.nrays() != K2.nrays(): + return False - INPUT: + if K1.dim() != K2.dim(): + return False - - ``min_dim`` (default: zero) -- A nonnegative integer representing the - minimum dimension of the ambient lattice. + if K1.lineality() != K2.lineality(): + return False - - ``max_dim`` (default: random) -- A nonnegative integer representing - the maximum dimension of the ambient - lattice. + if K1.is_solid() != K2.is_solid(): + return False - - ``min_rays`` (default: zero) -- A nonnegative integer representing the - minimum number of generating rays of the - cone. + if K1.is_strictly_convex() != K2.is_strictly_convex(): + return False - - ``max_rays`` (default: random) -- A nonnegative integer representing the - maximum number of generating rays of the - cone. + if len(LL(K1)) != len(LL(K2)): + return False - OUTPUT: + C_of_K1 = discrete_complementarity_set(K1) + C_of_K2 = discrete_complementarity_set(K2) + if len(C_of_K1) != len(C_of_K2): + return False - A new, randomly generated cone. + if len(K1.facets()) != len(K2.facets()): + return False + + return True - EXAMPLES: - If we set the lower/upper bounds to zero, then our result is - predictable:: - sage: random_cone(0,0,0,0) - 0-d cone in 0-d lattice N +def rho(K, K2=None): + r""" + Restrict ``K`` into its own span, or the span of another cone. + + INPUT: - In fact, as long as we ask for zero rays, we should be able to predict - the output when ``min_dim == max_dim``:: + - ``K2`` -- another cone whose lattice has the same rank as this + cone. + + OUTPUT: - sage: random_cone(min_dim=4, max_dim=4, min_rays=0, max_rays=0) - 0-d cone in 4-d lattice N + A new cone in a sublattice. + + EXAMPLES:: + + sage: K = Cone([(1,)]) + sage: rho(K) == K + True + + sage: K2 = Cone([(1,0)]) + sage: rho(K2).rays() + N(1) + in 1-d lattice N + sage: K3 = Cone([(1,0,0)]) + sage: rho(K3).rays() + N(1) + in 1-d lattice N + sage: rho(K2) == rho(K3) + True 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:: + The projected cone should always be solid:: - sage: from sage.geometry.cone import is_Cone # long time - sage: K = random_cone() # long time - sage: is_Cone(K) # long time + sage: set_random_seed() + sage: K = random_cone(max_dim = 8) + sage: K_S = rho(K) + sage: K_S.is_solid() True - Ensure that an exception is raised when either lower bound is greater - than its respective upper bound:: + And the resulting cone should live in a space having the same + dimension as the space we restricted it to:: - sage: random_cone(min_dim=5, max_dim=2) - Traceback (most recent call last): - ... - ValueError: max_dim must be greater than or equal to min_dim. + sage: set_random_seed() + sage: K = random_cone(max_dim = 8) + sage: K_S = rho(K, K.dual() ) + sage: K_S.lattice_dim() == K.dual().dim() + True - sage: random_cone(min_rays=5, max_rays=2) - Traceback (most recent call last): - ... - ValueError: max_rays must be greater than or equal to min_rays. + This function should not affect the dimension of a cone:: - """ + sage: set_random_seed() + sage: K = random_cone(max_dim = 8) + sage: K.dim() == rho(K).dim() + True + + Nor should it affect the lineality of a cone:: + + sage: set_random_seed() + sage: K = random_cone(max_dim = 8) + sage: K.lineality() == rho(K).lineality() + True + + No matter which space we restrict to, the lineality should not + increase:: + + sage: set_random_seed() + sage: K = random_cone(max_dim = 8) + sage: K.lineality() >= rho(K).lineality() + True + sage: K.lineality() >= rho(K, K.dual()).lineality() + True + + If we do this according to our paper, then the result is proper:: + + sage: set_random_seed() + sage: K = random_cone(max_dim = 8, strictly_convex=False, solid=False) + sage: K_S = rho(K) + sage: K_SP = rho(K_S.dual()).dual() + sage: K_SP.is_proper() + True + sage: K_SP = rho(K_S, K_S.dual()) + sage: K_SP.is_proper() + True + + :: + + sage: set_random_seed() + sage: K = random_cone(max_dim = 8, strictly_convex=True, solid=False) + sage: K_S = rho(K) + sage: K_SP = rho(K_S.dual()).dual() + sage: K_SP.is_proper() + True + sage: K_SP = rho(K_S, K_S.dual()) + sage: K_SP.is_proper() + True + + :: + + sage: set_random_seed() + sage: K = random_cone(max_dim = 8, strictly_convex=False, solid=True) + sage: K_S = rho(K) + sage: K_SP = rho(K_S.dual()).dual() + sage: K_SP.is_proper() + True + sage: K_SP = rho(K_S, K_S.dual()) + sage: K_SP.is_proper() + True + + :: - # Catch obvious mistakes so that we can generate clear error - # messages. + sage: set_random_seed() + sage: K = random_cone(max_dim = 8, strictly_convex=True, solid=True) + sage: K_S = rho(K) + sage: K_SP = rho(K_S.dual()).dual() + sage: K_SP.is_proper() + True + sage: K_SP = rho(K_S, K_S.dual()) + sage: K_SP.is_proper() + True + + Test the proposition in our paper concerning the duals and + restrictions. Generate a random cone, then create a subcone of + it. The operation of dual-taking should then commute with rho:: + + sage: set_random_seed() + sage: J = random_cone(max_dim = 8, solid=False, strictly_convex=False) + sage: K = Cone(random_sublist(J.rays(), 0.5), lattice=J.lattice()) + sage: K_W = rho(K, J) + sage: K_star_W_star = rho(K.dual(), J).dual() + sage: basically_the_same(K_W, K_star_W_star) + True - if min_dim < 0: - raise ValueError('min_dim must be nonnegative.') + :: - if min_rays < 0: - raise ValueError('min_rays must be nonnegative.') + sage: set_random_seed() + sage: J = random_cone(max_dim = 8, solid=True, strictly_convex=False) + sage: K = Cone(random_sublist(J.rays(), 0.5), lattice=J.lattice()) + sage: K_W = rho(K, J) + sage: K_star_W_star = rho(K.dual(), J).dual() + sage: basically_the_same(K_W, K_star_W_star) + True - if max_dim is not None: - if max_dim < 0: - raise ValueError('max_dim must be nonnegative.') - if (min_dim > max_dim): - raise ValueError('max_dim must be greater than or equal to min_dim.') + :: - if max_rays is not None: - if max_rays < 0: - raise ValueError('max_rays must be nonnegative.') - if (min_rays > max_rays): - raise ValueError('max_rays must be greater than or equal to min_rays.') + sage: set_random_seed() + sage: J = random_cone(max_dim = 8, solid=False, strictly_convex=True) + sage: K = Cone(random_sublist(J.rays(), 0.5), lattice=J.lattice()) + sage: K_W = rho(K, J) + sage: K_star_W_star = rho(K.dual(), J).dual() + sage: basically_the_same(K_W, K_star_W_star) + True + :: - def random_min_max(l,u): - r""" - We need to handle two cases for the upper bounds, and we need to do - the same thing for max_dim/max_rays. So we consolidate the logic here. - """ - if u is None: - # The upper bound is unspecified; return a random integer - # in [l,infinity). - return l + ZZ.random_element().abs() - else: - # We have an upper bound, and it's greater than or equal - # to our lower bound. So we generate a random integer in - # [0,u-l], and then add it to l to get something in - # [l,u]. To understand the "+1", check the - # ZZ.random_element() docs. - return l + ZZ.random_element(u - l + 1) + sage: set_random_seed() + sage: J = random_cone(max_dim = 8, solid=True, strictly_convex=True) + sage: K = Cone(random_sublist(J.rays(), 0.5), lattice=J.lattice()) + sage: K_W = rho(K, J) + sage: K_star_W_star = rho(K.dual(), J).dual() + sage: basically_the_same(K_W, K_star_W_star) + True + + """ + if K2 is None: + K2 = K + # First we project K onto the span of K2. This will explode if the + # rank of ``K2.lattice()`` doesn't match ours. + span_K2 = Cone(K2.rays() + (-K2).rays(), lattice=K.lattice()) + K = K.intersection(span_K2) - d = random_min_max(min_dim, max_dim) - r = random_min_max(min_rays, max_rays) + # Cheat a little to get the subspace span(K2). The paper uses the + # rays of K2 as a basis, but everything is invariant under linear + # isomorphism (i.e. a change of basis), and this is a little + # faster. + W = span_K2.linear_subspace() - L = ToricLattice(d) - rays = [L.random_element() for i in range(0,r)] + # We've already intersected K with the span of K2, so every + # generator of K should belong to W now. + W_rays = [ W.coordinate_vector(r) for r in K.rays() ] + + L = ToricLattice(K2.dim()) + return Cone(W_rays, lattice=L) - # The lattice parameter is required when no rays are given, so we - # pass it just in case. - return Cone(rays, lattice=L) def discrete_complementarity_set(K): @@ -141,9 +248,7 @@ def discrete_complementarity_set(K): 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. @@ -182,11 +287,12 @@ def discrete_complementarity_set(K): 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(max_dim=10, max_rays=10) + sage: set_random_seed() + sage: K1 = random_cone(max_dim=6) 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 + sage: sorted(actual) == sorted(expected) True """ @@ -200,6 +306,135 @@ def discrete_complementarity_set(K): return [(x,s) for x in xs for s in ss if x.inner_product(s) == 0] +def LL(K): + r""" + Compute the space `\mathbf{LL}` of all Lyapunov-like transformations + on this cone. + + OUTPUT: + + A list of matrices forming a basis for the space of all + Lyapunov-like transformations on the given cone. + + EXAMPLES: + + The trivial cone has no Lyapunov-like transformations:: + + sage: L = ToricLattice(0) + sage: K = Cone([], lattice=L) + sage: LL(K) + [] + + The Lyapunov-like transformations on the nonnegative orthant are + simply diagonal matrices:: + + sage: K = Cone([(1,)]) + sage: LL(K) + [[1]] + + sage: K = Cone([(1,0),(0,1)]) + sage: LL(K) + [ + [1 0] [0 0] + [0 0], [0 1] + ] + + sage: K = Cone([(1,0,0),(0,1,0),(0,0,1)]) + sage: LL(K) + [ + [1 0 0] [0 0 0] [0 0 0] + [0 0 0] [0 1 0] [0 0 0] + [0 0 0], [0 0 0], [0 0 1] + ] + + Only the identity matrix is Lyapunov-like on the `L^{3}_{1}` and + `L^{3}_{\infty}` cones [Rudolf et al.]_:: + + sage: L31 = Cone([(1,0,1), (0,-1,1), (-1,0,1), (0,1,1)]) + sage: LL(L31) + [ + [1 0 0] + [0 1 0] + [0 0 1] + ] + + sage: L3infty = Cone([(0,1,1), (1,0,1), (0,-1,1), (-1,0,1)]) + sage: LL(L3infty) + [ + [1 0 0] + [0 1 0] + [0 0 1] + ] + + If our cone is the entire space, then every transformation on it is + Lyapunov-like:: + + sage: K = Cone([(1,0), (-1,0), (0,1), (0,-1)]) + sage: M = MatrixSpace(QQ,2) + sage: M.basis() == LL(K) + True + + TESTS: + + The inner product `\left< L\left(x\right), s \right>` is zero for + every pair `\left( x,s \right)` in the discrete complementarity set + of the cone:: + + sage: set_random_seed() + sage: K = random_cone(max_dim=8) + sage: C_of_K = discrete_complementarity_set(K) + sage: l = [ (L*x).inner_product(s) for (x,s) in C_of_K for L in LL(K) ] + sage: sum(map(abs, l)) + 0 + + The Lyapunov-like transformations on a cone and its dual are related + by transposition, but we're not guaranteed to compute transposed + elements of `LL\left( K \right)` as our basis for `LL\left( K^{*} + \right)` + + sage: set_random_seed() + sage: K = random_cone(max_dim=8) + sage: LL2 = [ L.transpose() for L in LL(K.dual()) ] + sage: V = VectorSpace( K.lattice().base_field(), K.lattice_dim()^2) + sage: LL1_vecs = [ V(m.list()) for m in LL(K) ] + sage: LL2_vecs = [ V(m.list()) for m in LL2 ] + sage: V.span(LL1_vecs) == V.span(LL2_vecs) + True + + """ + V = K.lattice().vector_space() + + C_of_K = discrete_complementarity_set(K) + + tensor_products = [ s.tensor_product(x) 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) + + # Turn our matrices into long vectors... + vectors = [ W(m.list()) for m in tensor_products ] + + # Vector space representation of Lyapunov-like matrices + # (i.e. vec(L) where L is Luapunov-like). + LL_vector = W.span(vectors).complement() + + # Now construct an ambient MatrixSpace in which to stick our + # transformations. + M = MatrixSpace(V.base_ring(), V.dimension()) + + matrix_basis = [ M(v.list()) for v in LL_vector.basis() ] + + return matrix_basis + + + def lyapunov_rank(K): r""" Compute the Lyapunov (or bilinearity) rank of this cone. @@ -243,17 +478,21 @@ def lyapunov_rank(K): REFERENCES: - 1. M.S. Gowda and J. Tao. On the bilinearity rank of a proper cone - and Lyapunov-like transformations, Mathematical Programming, 147 + .. [Gowda/Tao] 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 + .. [Orlitzky/Gowda] M. Orlitzky and M. S. Gowda. The Lyapunov Rank of an + Improper Cone. Work in-progress. + + .. [Rudolf et al.] 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`:: + The nonnegative orthant in `\mathbb{R}^{n}` always has rank `n` + [Rudolf et al.]_:: sage: positives = Cone([(1,)]) sage: lyapunov_rank(positives) @@ -261,23 +500,57 @@ def lyapunov_rank(K): 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: 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:: + The full space `\mathbb{R}^{n}` has Lyapunov rank `n^{2}` + [Orlitzky/Gowda]_:: + + sage: R5 = VectorSpace(QQ, 5) + sage: gs = R5.basis() + [ -r for r in R5.basis() ] + sage: K = Cone(gs) + sage: lyapunov_rank(K) + 25 + + The `L^{3}_{1}` cone is known to have a Lyapunov rank of one + [Rudolf et al.]_:: 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:: + Likewise for the `L^{3}_{\infty}` cone [Rudolf et al.]_:: 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:: + A single ray in `n` dimensions should have Lyapunov rank `n^{2} - n + + 1` [Orlitzky/Gowda]_:: + + sage: K = Cone([(1,0,0,0,0)]) + sage: lyapunov_rank(K) + 21 + sage: K.lattice_dim()**2 - K.lattice_dim() + 1 + 21 + + A subspace (of dimension `m`) in `n` dimensions should have a + Lyapunov rank of `n^{2} - m\left(n - m)` [Orlitzky/Gowda]_:: + + sage: e1 = (1,0,0,0,0) + sage: neg_e1 = (-1,0,0,0,0) + sage: e2 = (0,1,0,0,0) + sage: neg_e2 = (0,-1,0,0,0) + sage: z = (0,0,0,0,0) + sage: K = Cone([e1, neg_e1, e2, neg_e2, z, z, z]) + sage: lyapunov_rank(K) + 19 + sage: K.lattice_dim()**2 - K.dim()*K.codim() + 19 + + The Lyapunov rank should be additive on a product of proper cones + [Rudolf et al.]_:: 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)]) @@ -285,8 +558,8 @@ def lyapunov_rank(K): 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 + Two isomorphic cones should have the same Lyapunov rank [Rudolf et al.]_. + 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)]) @@ -294,7 +567,7 @@ def lyapunov_rank(K): 3 The dual cone `K^{*}` of ``K`` should have the same Lyapunov rank as ``K`` - itself:: + itself [Rudolf et al.]_:: sage: K = Cone([(2,2,4), (-1,9,0), (2,0,6)]) sage: lyapunov_rank(K) == lyapunov_rank(K.dual()) @@ -302,44 +575,151 @@ def lyapunov_rank(K): TESTS: - The Lyapunov rank should be additive on a product of cones:: + The Lyapunov rank should be additive on a product of proper cones + [Rudolf et al.]_:: - sage: K1 = random_cone(max_dim=10, max_rays=10) - sage: K2 = random_cone(max_dim=10, max_rays=10) + sage: set_random_seed() + sage: K1 = random_cone(max_dim=8, strictly_convex=True, solid=True) + sage: K2 = random_cone(max_dim=8, strictly_convex=True, solid=True) 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:: + itself [Rudolf et al.]_:: - sage: K = random_cone(max_dim=10, max_rays=10) + sage: set_random_seed() + sage: K = random_cone(max_dim=8) sage: lyapunov_rank(K) == lyapunov_rank(K.dual()) True + Make sure we exercise the non-strictly-convex/non-solid case:: + + sage: set_random_seed() + sage: K = random_cone(max_dim=8, strictly_convex=False, solid=False) + sage: lyapunov_rank(K) == lyapunov_rank(K.dual()) + True + + Let's check the other permutations as well, just to be sure:: + + sage: set_random_seed() + sage: K = random_cone(max_dim=8, strictly_convex=False, solid=True) + sage: lyapunov_rank(K) == lyapunov_rank(K.dual()) + True + + :: + + sage: set_random_seed() + sage: K = random_cone(max_dim=8, strictly_convex=True, solid=False) + sage: lyapunov_rank(K) == lyapunov_rank(K.dual()) + True + + :: + + sage: set_random_seed() + sage: K = random_cone(max_dim=8, strictly_convex=True, solid=True) + sage: lyapunov_rank(K) == lyapunov_rank(K.dual()) + True + + The Lyapunov rank of a proper polyhedral cone in `n` dimensions can + be any number between `1` and `n` inclusive, excluding `n-1` + [Gowda/Tao]_. By accident, the `n-1` restriction will hold for the + trivial cone in a trivial space as well. However, in zero dimensions, + the Lyapunov rank of the trivial cone will be zero:: + + sage: set_random_seed() + sage: K = random_cone(max_dim=8, strictly_convex=True, solid=True) + sage: b = lyapunov_rank(K) + sage: n = K.lattice_dim() + sage: (n == 0 or 1 <= b) and b <= n + True + sage: b == n-1 + False + + In fact [Orlitzky/Gowda]_, no closed convex polyhedral cone can have + Lyapunov rank `n-1` in `n` dimensions:: + + sage: set_random_seed() + sage: K = random_cone(max_dim=8) + sage: b = lyapunov_rank(K) + sage: n = K.lattice_dim() + sage: b == n-1 + False + + The calculation of the Lyapunov rank of an improper cone can be + reduced to that of a proper cone [Orlitzky/Gowda]_:: + + sage: set_random_seed() + sage: K = random_cone(max_dim=8) + sage: actual = lyapunov_rank(K) + sage: K_S = rho(K) + sage: K_SP = rho(K_S.dual()).dual() + sage: l = K.lineality() + sage: c = K.codim() + sage: expected = lyapunov_rank(K_SP) + K.dim()*(l + c) + c**2 + sage: actual == expected + True + + The Lyapunov rank of a proper cone is just the dimension of ``LL(K)``:: + + sage: set_random_seed() + sage: K = random_cone(max_dim=8, strictly_convex=True, solid=True) + sage: lyapunov_rank(K) == len(LL(K)) + True + + In fact the same can be said of any cone. These additional tests + just increase our confidence that the reduction scheme works:: + + sage: set_random_seed() + sage: K = random_cone(max_dim=8, strictly_convex=True, solid=False) + sage: lyapunov_rank(K) == len(LL(K)) + True + + :: + + sage: set_random_seed() + sage: K = random_cone(max_dim=8, strictly_convex=False, solid=True) + sage: lyapunov_rank(K) == len(LL(K)) + True + + :: + + sage: set_random_seed() + sage: K = random_cone(max_dim=8, strictly_convex=False, solid=False) + sage: lyapunov_rank(K) == len(LL(K)) + True + + Test Theorem 3 in [Orlitzky/Gowda]_:: + + sage: set_random_seed() + sage: K = random_cone(max_dim=8, strictly_convex=True, solid=True) + sage: L = ToricLattice(K.lattice_dim() + 1) + sage: K = Cone([ r.list() + [0] for r in K.rays() ], lattice=L) + sage: lyapunov_rank(K) >= K.lattice_dim() + True + """ - V = K.lattice().vector_space() + beta = 0 - C_of_K = discrete_complementarity_set(K) + m = K.dim() + n = K.lattice_dim() + l = K.lineality() - matrices = [x.tensor_product(s) for (x,s) in C_of_K] + if m < n: + # K is not solid, restrict to its span. + K = rho(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) + # Lemma 2 + beta += m*(n - m) + (n - m)**2 - def phi(m): - r""" - Convert a matrix to a vector isomorphically. - """ - return W(m.list()) + if l > 0: + # K is not pointed, restrict to the span of its dual. Uses a + # proposition from our paper, i.e. this is equivalent to K = + # rho(K.dual()).dual(). + K = rho(K, K.dual()) - vectors = [phi(m) for m in matrices] + # Lemma 3 + beta += m * l - return (W.dimension() - W.span(vectors).rank()) + beta += len(LL(K)) + return beta