@@ -193,6 +193,322 @@ def project_points(*points, **kwds):
193193 return [m * v for v in vecs ]
194194
195195
196+ def gale_transform_to_polytope (vectors , base_ring = None , backend = None ):
197+ r"""
198+ Return the polytope associated to the list of vectors forming a Gale transform.
199+
200+ This function is the inverse of
201+ :meth:`~sage.geometry.polyhedron.base.Polyhedron_base.gale_transform`
202+ up to projective transformation.
203+
204+ INPUT:
205+
206+ - ``vectors`` -- the vectors of the Gale transform
207+
208+ - ``base_ring`` -- string (default: `None`);
209+ the base ring to be used for the construction
210+
211+ - ``backend`` -- string (default: `None`);
212+ the backend to use to create the polytope
213+
214+ .. NOTE::
215+
216+ The order of the input vectors will not be preserved.
217+
218+ If the center of the (input) vectors is the origin,
219+ the function is much faster and might give a nicer representation
220+ of the polytope.
221+
222+ If this is not the case, the vectors will be scaled
223+ (each by a positive scalar) accordingly to obtain the polytope.
224+
225+ .. SEEALSO::
226+
227+ :func`~sage.geometry.polyhedron.library.gale_transform_to_primal`.
228+
229+ EXAMPLES::
230+
231+ sage: from sage.geometry.polyhedron.library import gale_transform_to_polytope
232+ sage: points = polytopes.octahedron().gale_transform()
233+ sage: points
234+ ((0, -1), (-1, 0), (1, 1), (1, 1), (-1, 0), (0, -1))
235+ sage: P = gale_transform_to_polytope(points); P
236+ A 3-dimensional polyhedron in ZZ^3 defined as the convex hull of 6 vertices
237+ sage: P.vertices()
238+ (A vertex at (-1, 0, 0),
239+ A vertex at (0, -1, 0),
240+ A vertex at (0, 0, -1),
241+ A vertex at (0, 0, 1),
242+ A vertex at (0, 1, 0),
243+ A vertex at (1, 0, 0))
244+
245+ One can specify the base ring::
246+
247+ sage: gale_transform_to_polytope(
248+ ....: [(1,1), (-1,-1), (1,0),
249+ ....: (-1,0), (1,-1), (-2,1)]).vertices()
250+ (A vertex at (-25, 0, 0),
251+ A vertex at (-15, 50, -60),
252+ A vertex at (0, -25, 0),
253+ A vertex at (0, 0, -25),
254+ A vertex at (16, -35, 54),
255+ A vertex at (24, 10, 31))
256+ sage: gale_transform_to_polytope(
257+ ....: [(1,1), (-1,-1), (1,0),
258+ ....: (-1,0), (1,-1), (-2,1)],
259+ ....: base_ring=RDF).vertices()
260+ (A vertex at (-0.64, 1.4, -2.16),
261+ A vertex at (-0.96, -0.4, -1.24),
262+ A vertex at (0.6, -2.0, 2.4),
263+ A vertex at (1.0, 0.0, 0.0),
264+ A vertex at (0.0, 1.0, 0.0),
265+ A vertex at (0.0, 0.0, 1.0))
266+
267+ One can also specify the backend::
268+
269+ sage: gale_transform_to_polytope(
270+ ....: [(1,1), (-1,-1), (1,0),
271+ ....: (-1,0), (1,-1), (-2,1)],
272+ ....: backend='field').backend()
273+ 'field'
274+ sage: gale_transform_to_polytope(
275+ ....: [(1,1), (-1,-1), (1,0),
276+ ....: (-1,0), (1,-1), (-2,1)],
277+ ....: backend='cdd', base_ring=RDF).backend()
278+ 'cdd'
279+
280+ A gale transform corresponds to a polytope if and only if
281+ every oriented (linear) hyperplane
282+ has at least two vectors on each side.
283+ See Theorem 6.19 of [Zie2007]_.
284+ If this is not the case, one of two errors is raised.
285+
286+ If there is such a hyperplane with no vector on one side,
287+ the vectors are not totally cyclic::
288+
289+ sage: gale_transform_to_polytope([(0,1), (1,1), (1,0), (-1,0)])
290+ Traceback (most recent call last):
291+ ...
292+ ValueError: input vectors not totally cyclic
293+
294+ If every hyperplane has at least one vector on each side, then the gale
295+ transform corresponds to a point configuration.
296+ It corresponds to a polytope if and only if this point configuration is
297+ convex and if and only if every hyperplane contains at least two vectors of
298+ the gale transform on each side.
299+
300+ If this is not the case, an error is raised::
301+
302+ sage: gale_transform_to_polytope([(0,1), (1,1), (1,0), (-1,-1)])
303+ Traceback (most recent call last):
304+ ...
305+ ValueError: the gale transform does not correspond to a polytope
306+
307+ TESTS::
308+
309+ sage: def test(P):
310+ ....: P1 = gale_transform_to_polytope(
311+ ....: P.gale_transform(), base_ring=P.base_ring(),
312+ ....: backend=P.backend())
313+ ....: assert P1.is_combinatorially_isomorphic(P)
314+
315+ sage: test(polytopes.cube())
316+ sage: test(polytopes.permutahedron(4))
317+ sage: test(polytopes.regular_polygon(5))
318+ sage: test(polytopes.regular_polygon(7, exact=False))
319+ sage: test(polytopes.snub_cube(exact=True, backend='normaliz')) # optional - pynormaliz
320+ """
321+ vertices = gale_transform_to_primal (vectors , base_ring , backend )
322+ P = Polyhedron (vertices = vertices , base_ring = base_ring , backend = backend )
323+
324+ if not P .n_vertices () == len (vertices ):
325+ # If the input vectors are not totally cyclic, ``gale_transform_to_primal``
326+ # raises an error.
327+ # As no error was raised so far, the gale transform corresponds to
328+ # to a point configuration.
329+ # It corresponds to a polytope if and only if
330+ # ``vertices`` are in convex position.
331+ raise ValueError ("the gale transform does not correspond to a polytope" )
332+
333+ return P
334+
335+ def gale_transform_to_primal (vectors , base_ring = None , backend = None ):
336+ r"""
337+ Return a point configuration dual to a totally cyclic vector configuration.
338+
339+ This is the dehomogenized vector configuration dual to the input.
340+ The dual vector configuration is acyclic and can therefore
341+ be dehomogenized as the input is totally cyclic.
342+
343+ INPUT:
344+
345+ - ``vectors`` -- the ordered vectors of the Gale transform
346+
347+ - ``base_ring`` -- string (default: `None`);
348+ the base ring to be used for the construction
349+
350+ - ``backend`` -- string (default: `None`);
351+ the backend to be use to construct a polyhedral,
352+ used interally in case the center is not the origin,
353+ see :func:`~sage.geometry.polyhedron.constructor.Polyhedron`
354+
355+ OUTPUT: An ordered point configuration as list of vectors.
356+
357+ .. NOTE::
358+
359+ If the center of the (input) vectors is the origin,
360+ the function is much faster and might give a nicer representation
361+ of the point configuration.
362+
363+ If this is not the case, the vectors will be scaled
364+ (each by a positive scalar) accordingly.
365+
366+ ALGORITHM:
367+
368+ Step 1: If the center of the (input) vectors is not the origin,
369+ we do an appropriate transformation to make it so.
370+
371+ Step 2: We add a row of ones on top of ``Matrix(vectors)``.
372+ The right kernel of this larger matrix is the dual configuration space,
373+ and a basis of this space provides the dual point configuration.
374+
375+ More concretely, the dual vector configuration (inhomogeneous)
376+ is obtained by taking a basis of the right kernel of ``Matrix(vectors)``.
377+ If the center of the (input) vectors is the origin,
378+ there exists a basis of the right kernel of the form
379+ ``[[1], [V]]``, where ``[1]`` represents a row of ones.
380+ Then, ``V`` is a dehomogenization and thus the dual point configuration.
381+
382+ To extend ``[1]`` to a basis of ``Matrix(vectors)``, we add
383+ a row of ones to ``Matrix(vectors)`` and calculate a basis of the
384+ right kernel of the obtained matrix.
385+
386+ REFERENCES:
387+
388+ For more information, see Section 6.4 of [Zie2007]_
389+ or Definition 2.5.1 and Definition 4.1.35 of [DLRS2010]_.
390+
391+ .. SEEALSO::
392+
393+ :func`~sage.geometry.polyhedron.library.gale_transform_to_polytope`.
394+
395+ EXAMPLES::
396+
397+ sage: from sage.geometry.polyhedron.library import gale_transform_to_primal
398+ sage: points = ((0, -1), (-1, 0), (1, 1), (1, 1), (-1, 0), (0, -1))
399+ sage: gale_transform_to_primal(points)
400+ [(0, 0, 1), (0, 1, 0), (1, 0, 0), (-1, 0, 0), (0, -1, 0), (0, 0, -1)]
401+
402+ One can specify the base ring::
403+
404+ sage: gale_transform_to_primal(
405+ ....: [(1,1), (-1,-1), (1,0),
406+ ....: (-1,0), (1,-1), (-2,1)])
407+ [(16, -35, 54),
408+ (24, 10, 31),
409+ (-15, 50, -60),
410+ (-25, 0, 0),
411+ (0, -25, 0),
412+ (0, 0, -25)]
413+ sage: gale_transform_to_primal(
414+ ....: [(1,1),(-1,-1),(1,0),(-1,0),(1,-1),(-2,1)], base_ring=RDF)
415+ [(-0.6400000000000001, 1.4, -2.1600000000000006),
416+ (-0.9600000000000002, -0.39999999999999997, -1.2400000000000002),
417+ (0.6000000000000001, -2.0, 2.4000000000000004),
418+ (1.0, 0.0, 0.0),
419+ (0.0, 1.0, 0.0),
420+ (0.0, 0.0, 1.0)]
421+
422+ One can also specify the backend to be used internally::
423+
424+ sage: gale_transform_to_primal(
425+ ....: [(1,1), (-1,-1), (1,0),
426+ ....: (-1,0), (1,-1), (-2,1)], backend='field')
427+ [(48, -71, 88),
428+ (84, -28, 99),
429+ (-77, 154, -132),
430+ (-55, 0, 0),
431+ (0, -55, 0),
432+ (0, 0, -55)]
433+ sage: gale_transform_to_primal( # optional - pynormaliz
434+ ....: [(1,1), (-1,-1), (1,0),
435+ ....: (-1,0), (1,-1), (-2,1)], backend='normaliz')
436+ [(16, -35, 54),
437+ (24, 10, 31),
438+ (-15, 50, -60),
439+ (-25, 0, 0),
440+ (0, -25, 0),
441+ (0, 0, -25)]
442+
443+ The input vectors should be totally cyclic::
444+
445+ sage: gale_transform_to_primal([(0,1), (1,0), (1,1), (-1,0)])
446+ Traceback (most recent call last):
447+ ...
448+ ValueError: input vectors not totally cyclic
449+
450+ sage: gale_transform_to_primal(
451+ ....: [(1,1,0), (-1,-1,0), (1,0,0),
452+ ....: (-1,0,0), (1,-1,0), (-2,1,0)], backend='field')
453+ Traceback (most recent call last):
454+ ...
455+ ValueError: input vectors not totally cyclic
456+ """
457+ from sage .modules .free_module_element import vector
458+ from sage .matrix .all import Matrix
459+ if base_ring :
460+ vectors = tuple (vector (base_ring , x ) for x in vectors )
461+ else :
462+ vectors = tuple (vector (x ) for x in vectors )
463+
464+ if not sum (vectors ).is_zero ():
465+ # The center of the input vectors shall be the origin.
466+ # If this is not the case, we scale them accordingly.
467+ # This has the adventage that right kernel of ``vectors`` can be
468+ # presented in the form ``[[1], [V]]``, where ``V`` are the points
469+ # in the dual point configuration.
470+ # (Dehomogenization is straightforward.)
471+
472+ # Scaling of the vectors is equivalent to finding a hyperplane that intersects
473+ # all vectors of the dual point configuration. But if the input is already provided
474+ # such that the vectors add up to zero, the coordinates might be nicer.
475+ # (And this is faster.)
476+
477+ if base_ring :
478+ ker = Matrix (base_ring , vectors ).left_kernel ()
479+ else :
480+ ker = Matrix (vectors ).left_kernel ()
481+ solutions = Polyhedron (lines = tuple (y for y in ker .basis_matrix ()), base_ring = base_ring , backend = backend )
482+
483+ from sage .matrix .special import identity_matrix
484+ pos_orthant = Polyhedron (rays = identity_matrix (len (vectors )), base_ring = base_ring , backend = backend )
485+ pos_solutions = solutions .intersection (pos_orthant )
486+ if base_ring is ZZ :
487+ pos_solutions = pos_solutions .change_ring (ZZ )
488+
489+ # Any integral point in ``pos_solutions`` will correspond to scaling-factors
490+ # that make ``sum(vectors)`` zero.
491+ x = pos_solutions .representative_point ()
492+ if not all (y > 0 for y in x ):
493+ raise ValueError ("input vectors not totally cyclic" )
494+ vectors = tuple (vec * x [i ] for i ,vec in enumerate (vectors ))
495+
496+ # The right kernel of ``vectors`` has a basis of the form ``[[1], [V]]``,
497+ # where ``V`` is the dehomogenized dual point configuration.
498+ # If we append a row of ones to ``vectors``, ``V`` is just the right kernel.
499+ if base_ring :
500+ m = Matrix (base_ring , vectors ).transpose ().stack (Matrix (base_ring , [[1 ]* len (vectors )]))
501+ else :
502+ m = Matrix (vectors ).transpose ().stack (Matrix ([[1 ]* len (vectors )]))
503+
504+ if m .rank () != len (vectors [0 ]) + 1 :
505+ # The given vectors do not span the ambient space,
506+ # then there exists a nonnegative value vector.
507+ raise ValueError ("input vectors not totally cyclic" )
508+
509+ return m .right_kernel_matrix (basis = 'computed' ).columns ()
510+
511+
196512class Polytopes ():
197513 """
198514 A class of constructors for commonly used, famous, or interesting
0 commit comments