+def _basically_the_same(K1, K2):
+ r"""
+ Test whether or not ``K1`` and ``K2`` are "basically the same."
+
+ This is a hack to get around the fact that it's difficult to tell
+ when two cones are linearly isomorphic. We have a proposition that
+ equates two cones, but represented over `\mathbb{Q}`, they are
+ merely linearly isomorphic (not equal). So rather than test for
+ equality, we test a list of properties that should be preserved
+ under an invertible linear transformation.
+
+ OUTPUT:
+
+ ``True`` if ``K1`` and ``K2`` are basically the same, and ``False``
+ otherwise.
+
+ EXAMPLES:
+
+ Any proper cone with three generators in `\mathbb{R}^{3}` is
+ basically the same as the nonnegative orthant::
+
+ sage: K1 = Cone([(1,0,0), (0,1,0), (0,0,1)])
+ sage: K2 = Cone([(1,2,3), (3, 18, 4), (66, 51, 0)])
+ sage: _basically_the_same(K1, K2)
+ True
+
+ Negating a cone gives you another cone that is basically the same::
+
+ sage: K = Cone([(0,2,-5), (-6, 2, 4), (0, 51, 0)])
+ sage: _basically_the_same(K, -K)
+ True
+
+ TESTS:
+
+ Any cone is basically the same as itself::
+
+ sage: K = random_cone(max_ambient_dim = 8)
+ sage: _basically_the_same(K, K)
+ True
+
+ After applying an invertible matrix to the rows of a cone, the
+ result should be basically the same as the cone we started with::
+
+ sage: K1 = random_cone(max_ambient_dim = 8)
+ sage: A = random_matrix(QQ, K1.lattice_dim(), algorithm='unimodular')
+ sage: K2 = Cone( [ A*r for r in K1.rays() ], lattice=K1.lattice())
+ sage: _basically_the_same(K1, K2)
+ True
+
+ """
+ if K1.lattice_dim() != K2.lattice_dim():
+ return False
+
+ if K1.nrays() != K2.nrays():
+ return False
+
+ if K1.dim() != K2.dim():
+ return False
+
+ if K1.lineality() != K2.lineality():
+ return False
+
+ if K1.is_solid() != K2.is_solid():
+ return False
+
+ if K1.is_strictly_convex() != K2.is_strictly_convex():
+ return False
+
+ if len(LL(K1)) != len(LL(K2)):
+ return False
+
+ 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
+
+ if len(K1.facets()) != len(K2.facets()):
+ return False
+
+ return True
+
+
+
+def _restrict_to_space(K, W):
+ r"""
+ Restrict this cone a subspace of its ambient space.
+
+ INPUT:
+
+ - ``W`` -- The subspace into which this cone will be restricted.
+
+ OUTPUT:
+
+ A new cone in a sublattice corresponding to ``W``.
+
+ EXAMPLES:
+
+ When this cone is solid, restricting it into its own span should do
+ nothing::
+
+ sage: K = Cone([(1,)])
+ sage: _restrict_to_space(K, K.span()) == K
+ True
+
+ A single ray restricted into its own span gives the same output
+ regardless of the ambient space::
+
+ sage: K2 = Cone([(1,0)])
+ sage: K2_S = _restrict_to_space(K2, K2.span()).rays()
+ sage: K2_S
+ N(1)
+ in 1-d lattice N
+ sage: K3 = Cone([(1,0,0)])
+ sage: K3_S = _restrict_to_space(K3, K3.span()).rays()
+ sage: K3_S
+ N(1)
+ in 1-d lattice N
+ sage: K2_S == K3_S
+ True
+
+ TESTS:
+
+ The projected cone should always be solid::
+
+ sage: set_random_seed()
+ sage: K = random_cone(max_ambient_dim = 8)
+ sage: _restrict_to_space(K, K.span()).is_solid()
+ True
+
+ And the resulting cone should live in a space having the same
+ dimension as the space we restricted it to::
+
+ sage: set_random_seed()
+ sage: K = random_cone(max_ambient_dim = 8)
+ sage: K_P = _restrict_to_space(K, K.dual().span())
+ sage: K_P.lattice_dim() == K.dual().dim()
+ True
+
+ This function should not affect the dimension of a cone::
+
+ sage: set_random_seed()
+ sage: K = random_cone(max_ambient_dim = 8)
+ sage: K.dim() == _restrict_to_space(K,K.span()).dim()
+ True
+
+ Nor should it affect the lineality of a cone::
+
+ sage: set_random_seed()
+ sage: K = random_cone(max_ambient_dim = 8)
+ sage: K.lineality() == _restrict_to_space(K, K.span()).lineality()
+ True
+
+ No matter which space we restrict to, the lineality should not
+ increase::
+
+ sage: set_random_seed()
+ sage: K = random_cone(max_ambient_dim = 8)
+ sage: S = K.span(); P = K.dual().span()
+ sage: K.lineality() >= _restrict_to_space(K,S).lineality()
+ True
+ sage: K.lineality() >= _restrict_to_space(K,P).lineality()
+ True
+
+ If we do this according to our paper, then the result is proper::
+
+ sage: set_random_seed()
+ sage: K = random_cone(max_ambient_dim = 8)
+ sage: K_S = _restrict_to_space(K, K.span())
+ sage: K_SP = _restrict_to_space(K_S.dual(), K_S.dual().span()).dual()
+ sage: K_SP.is_proper()
+ True
+ sage: K_SP = _restrict_to_space(K_S, K_S.dual().span())
+ 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
+ _restrict_to_space::
+
+ sage: set_random_seed()
+ sage: J = random_cone(max_ambient_dim = 8)
+ sage: K = Cone(random_sublist(J.rays(), 0.5), lattice=J.lattice())
+ sage: K_W_star = _restrict_to_space(K, J.span()).dual()
+ sage: K_star_W = _restrict_to_space(K.dual(), J.span())
+ sage: _basically_the_same(K_W_star, K_star_W)
+ True
+
+ """
+ # First we want to intersect ``K`` with ``W``. The easiest way to
+ # do this is via cone intersection, so we turn the subspace ``W``
+ # into a cone.
+ W_cone = Cone(W.basis() + [-b for b in W.basis()], lattice=K.lattice())
+ K = K.intersection(W_cone)
+
+ # We've already intersected K with the span of K2, so every
+ # generator of K should belong to W now.
+ K_W_rays = [ W.coordinate_vector(r) for r in K.rays() ]
+
+ L = ToricLattice(W.dimension())
+ return Cone(K_W_rays, lattice=L)
+
+
+