# Ticket #11379: trac_11379_quantamino-total-sl.patch

File trac_11379_quantamino-total-sl.patch, 87.0 KB (added by rbeezer, 10 years ago)

Standalone, comprehensive patch

• ## doc/en/reference/combinat/index.rst

# HG changeset patch
# User Sebastien Labbe <slabqc at gmail.com>
# Date 1306440157 14400
# Node ID c7272c05107751335ca6a301d31a1e4d6406e44b
11379: Family Games America's Quantumino solver

diff --git a/doc/en/reference/combinat/index.rst b/doc/en/reference/combinat/index.rst
 a ../sage/combinat/skew_partition ../sage/combinat/subset ../sage/combinat/subword ../sage/combinat/tiling ../sage/combinat/tuple algebra
• ## doc/en/reference/games.rst

diff --git a/doc/en/reference/games.rst b/doc/en/reference/games.rst
 a .. toctree:: :maxdepth: 2 sage/games/sudoku No newline at end of file sage/games/sudoku sage/games/quantumino
• ## new file sage/combinat/tiling.py

diff --git a/sage/combinat/tiling.py b/sage/combinat/tiling.py
new file mode 100644
 - r""" Tiling Solver Tiling a n-dimensional box into non-intersecting n-dimensional polyominoes. This uses dancing links code which is in Sage.  Dancing links were originally introduced by Donald Knuth in 2000 [1]. In particular, Knuth used dancing links to solve tilings of a region by 2D pentaminos.  Here we extend the method to any dimension. In particular, the :mod:sage.games.quantumino module is based on the Tiling Solver and allows to solve the 3d Quantumino puzzle. This module defines two classes: - :class:sage.combinat.tiling.Polyomino class, to represent polyominoes in arbitrary dimension. The goal of this class is to return all the rotated, reflected and/or translated copies of a polyomino that are contained in a certain box. - :class:sage.combinat.tiling.TilingSolver class, to solve the general problem of tiling a rectangular n-dimensional box with a set of n-dimensional polyominoes. One can specify if rotations and reflections are allowed or not and if pieces can be reused or not. This class convert the tiling data into rows of a matrix that are passed to the DLX solver. It also allows to compute the number of solutions. AUTHOR: - Sebastien Labbe, June 2011 EXAMPLES: 2d Easy Example --------------- Here is a 2d example. Let's try to fill the 3 \times 2 rectangle with a 1 \times 2 rectangle and a 2 \times 2 square. Obviously, there are two solutions:: sage: from sage.combinat.tiling import TilingSolver, Polyomino sage: p = Polyomino([(0,0), (0,1)]) sage: q = Polyomino([(0,0), (0,1), (1,0), (1,1)]) sage: T = TilingSolver([p,q], box=[3,2]) sage: it = T.solve() sage: it.next() [Polyomino: [(0, 0), (0, 1), (1, 0), (1, 1)], Color: gray, Polyomino: [(2, 0), (2, 1)], Color: gray] sage: it.next() [Polyomino: [(1, 0), (1, 1), (2, 0), (2, 1)], Color: gray, Polyomino: [(0, 0), (0, 1)], Color: gray] sage: it.next() Traceback (most recent call last): ... StopIteration sage: T.number_of_solutions() 2 1d Easy Example --------------- Here is an easy one dimensional example where we try to tile a stick of length 6 with three sticks of length 1, 2 and 3. There are six solutions:: sage: p = Polyomino([[0]]) sage: q = Polyomino([[0],[1]]) sage: r = Polyomino([[0],[1],[2]]) sage: T = TilingSolver([p,q,r], box=[6]) sage: len(T.rows()) 15 sage: it = T.solve() sage: it.next() [Polyomino: [(0,)], Color: gray, Polyomino: [(1,), (2,)], Color: gray, Polyomino: [(3,), (4,), (5,)], Color: gray] sage: it.next() [Polyomino: [(0,)], Color: gray, Polyomino: [(1,), (2,), (3,)], Color: gray, Polyomino: [(4,), (5,)], Color: gray] sage: T.number_of_solutions() 6 2d Puzzle allowing reflections ------------------------------ The following is a puzzle owned by Florent Hivert:: sage: from sage.combinat.tiling import Polyomino, TilingSolver sage: L = [] sage: L.append(Polyomino([(0,0),(0,1),(0,2),(0,3),(1,0),(1,1),(1,2),(1,3)], 'yellow')) sage: L.append(Polyomino([(0,0),(0,1),(0,2),(0,3),(1,0),(1,1),(1,2)], "black")) sage: L.append(Polyomino([(0,0),(0,1),(0,2),(0,3),(1,0),(1,1),(1,3)], "gray")) sage: L.append(Polyomino([(0,0),(0,1),(0,2),(0,3),(1,0),(1,3)],"cyan")) sage: L.append(Polyomino([(0,0),(0,1),(0,2),(0,3),(1,0),(1,1)],"red")) sage: L.append(Polyomino([(0,0),(0,1),(0,2),(0,3),(1,1),(1,2)],"blue")) sage: L.append(Polyomino([(0,0),(0,1),(0,2),(0,3),(1,1),(1,3)],"green")) sage: L.append(Polyomino([(0,1),(0,2),(0,3),(1,0),(1,1),(1,3)],"magenta")) sage: L.append(Polyomino([(0,1),(0,2),(0,3),(1,0),(1,1),(1,2)],"orange")) sage: L.append(Polyomino([(0,0),(0,1),(0,2),(1,0),(1,1),(1,2)],"pink")) By default, rotations are allowed and reflections are not. In this case, there are no solution:: sage: T = TilingSolver(L, (8,8)) sage: T.number_of_solutions()                             # long time (2.5 s) 0 If reflections are allowed, there are solutions. Solve the puzzle and show one solution:: sage: T = TilingSolver(L, (8,8), reflection=True) sage: solution = T.solve().next() sage: G = sum([piece.show2d() for piece in solution], Graphics()) sage: G.show(aspect_ratio=1, axes=False) Compute the number of solutions:: sage: T.number_of_solutions()                              # long time (2.6s) 328 Create a animation of all the solutions:: sage: a = T.animate()                            # not tested sage: a                                          # not tested Animation with 328 frames 3d Puzzle --------- The same thing done in 3d *without* allowing reflections this time:: sage: from sage.combinat.tiling import Polyomino, TilingSolver sage: L = [] sage: L.append(Polyomino([(0,0,0),(0,1,0),(0,2,0),(0,3,0),(1,0,0),(1,1,0),(1,2,0),(1,3,0)])) sage: L.append(Polyomino([(0,0,0),(0,1,0),(0,2,0),(0,3,0),(1,0,0),(1,1,0),(1,2,0)])) sage: L.append(Polyomino([(0,0,0),(0,1,0),(0,2,0),(0,3,0),(1,0,0),(1,1,0),(1,3,0)])) sage: L.append(Polyomino([(0,0,0),(0,1,0),(0,2,0),(0,3,0),(1,0,0),(1,3,0)])) sage: L.append(Polyomino([(0,0,0),(0,1,0),(0,2,0),(0,3,0),(1,0,0),(1,1,0)])) sage: L.append(Polyomino([(0,0,0),(0,1,0),(0,2,0),(0,3,0),(1,1,0),(1,2,0)])) sage: L.append(Polyomino([(0,0,0),(0,1,0),(0,2,0),(0,3,0),(1,1,0),(1,3,0)])) sage: L.append(Polyomino([(0,1,0),(0,2,0),(0,3,0),(1,0,0),(1,1,0),(1,3,0)])) sage: L.append(Polyomino([(0,1,0),(0,2,0),(0,3,0),(1,0,0),(1,1,0),(1,2,0)])) sage: L.append(Polyomino([(0,0,0),(0,1,0),(0,2,0),(1,0,0),(1,1,0),(1,2,0)])) Solve the puzzle and show one solution:: sage: T = TilingSolver(L, (8,8,1)) sage: solution = T.solve().next() sage: G = sum([piece.show3d(size=0.85) for piece in solution], Graphics()) sage: G.show(aspect_ratio=1, viewer='tachyon') Let's compute the number of solutions:: sage: T.number_of_solutions()                              # long time (3s) 328 Donald Knuth example : the Y pentamino -------------------------------------- Donald Knuth [1] considered the problem of packing 45 Y pentominoes into a 15 \times 15 square:: sage: from sage.combinat.tiling import Polyomino, TilingSolver sage: y = Polyomino([(0,0),(1,0),(2,0),(3,0),(2,1)]) sage: T = TilingSolver([y], box=(5,10), reusable=True, reflection=True) sage: T.number_of_solutions() 10 sage: solution = T.solve().next() sage: G = sum([piece.show2d() for piece in solution], Graphics()) sage: G.show(aspect_ratio=1) :: sage: T = TilingSolver([y], box=(15,15), reusable=True, reflection=True) sage: T.number_of_solutions()                      #not tested 212 Animation of Donald Knuth's dancing links ----------------------------------------- Animation of the solutions:: sage: from sage.combinat.tiling import Polyomino, TilingSolver sage: Y = Polyomino([(0,0),(1,0),(2,0),(3,0),(2,1)], color='yellow') sage: T = TilingSolver([Y], box=(15,15), reusable=True, reflection=True) sage: a = T.animate(stop=40)            # long time sage: a                                 # long time Animation with 40 frames sage: a.show()                          # not tested  - requires convert command Incremental animation of the solutions (one piece is removed/added at a time):: sage: a = T.animate('incremental', stop=40)   # long time sage: a                                       # long time Animation with 40 frames sage: a.show(delay=50, iterations=1)     # not tested  - requires convert command 5d Easy Example --------------- Here is a 5d example. Let's try to fill the 2 \times 2 \times 2 \times 2 \times 2 rectangle with reusable 1 \times 1 \times 1 \times 1 \times 1 rectangles. Obviously, there is one solution:: sage: from sage.combinat.tiling import Polyomino, TilingSolver sage: p = Polyomino([(0,0,0,0,0)]) sage: T = TilingSolver([p], box=(2,2,2,2,2), reusable=True) sage: rows = T.rows()                               # long time (3s) sage: rows                                          # long time (fast) [[0], [1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11], [12], [13], [14], [15], [16], [17], [18], [19], [20], [21], [22], [23], [24], [25], [26], [27], [28], [29], [30], [31]] sage: T.number_of_solutions()                       # long time (fast) 1 REFERENCES: - [1] Knuth, Donald (2000). "Dancing links". arXiv:cs/0011047 _. """ #***************************************************************************** #       Copyright (C) 2011 Sebastien Labbe # #  Distributed under the terms of the GNU General Public License (GPL) #  as published by the Free Software Foundation; either version 2 of #  the License, or (at your option) any later version. #                  http://www.gnu.org/licenses/ #***************************************************************************** import itertools from sage.structure.sage_object import SageObject from sage.misc.cachefunc import cached_method, cached_function from sage.misc.misc import prod from sage.combinat.all import WeylGroup from sage.plot.plot import Graphics from sage.plot.polygon import polygon from sage.plot.line import line from sage.plot.circle import circle from sage.modules.free_module_element import vector from sage.plot.plot3d.platonic import cube from sage.plot.animate import Animation from sage.misc.mrange import xmrange ############################ # Orthogonal transformations ############################ @cached_function def orthogonal_transformation(n, orientation_preserving=True): r""" Return the list of orthogonal transformation matrices in the n-dimensional vector space. INPUT: - n - positive integer, dimension of the space - orientation_preserving - bool (optional, default: True), whether the orientation is preserved OUTPUT: list of matrices EXAMPLES:: sage: from sage.combinat.tiling import orthogonal_transformation sage: orthogonal_transformation(2) [ [1 0]  [ 0 -1]  [ 0  1]  [-1  0] [0 1], [ 1  0], [-1  0], [ 0 -1] ] sage: orthogonal_transformation(2, orientation_preserving=False) [ [1 0]  [0 1]  [ 0 -1]  [-1  0]  [ 1  0]  [ 0  1]  [ 0 -1]  [-1  0] [0 1], [1 0], [ 1  0], [ 0  1], [ 0 -1], [-1  0], [-1  0], [ 0 -1] ] sage: orthogonal_transformation(3) [ [1 0 0]  [0 0 1]  [ 0  0 -1]  [-1  0  0]  [ 0 -1  0]  [0 1 0] [0 1 0]  [1 0 0]  [ 0  1  0]  [ 0  0  1]  [ 1  0  0]  [0 0 1] [0 0 1], [0 1 0], [ 1  0  0], [ 0  1  0], [ 0  0  1], [1 0 0], [ 1  0  0]  [ 0  0  1]  [ 0  0 -1]  [-1  0  0]  [ 0 -1  0]  [ 0  1  0] [ 0  0 -1]  [ 0 -1  0]  [-1  0  0]  [ 0 -1  0]  [ 0  0 -1]  [-1  0  0] [ 0  1  0], [ 1  0  0], [ 0  1  0], [ 0  0  1], [ 1  0  0], [ 0  0  1], [ 0  1  0]  [ 0  0  1]  [ 0  0 -1]  [ 0 -1  0]  [-1  0  0]  [ 1  0  0] [ 1  0  0]  [ 0  1  0]  [ 1  0  0]  [ 0  0  1]  [ 0  1  0]  [ 0  0  1] [ 0  0 -1], [-1  0  0], [ 0 -1  0], [-1  0  0], [ 0  0 -1], [ 0 -1  0], [ 0  1  0]  [ 0  0  1]  [ 0  0 -1]  [ 0 -1  0]  [-1  0  0]  [ 1  0  0] [ 0  0 -1]  [-1  0  0]  [ 0 -1  0]  [-1  0  0]  [ 0  0 -1]  [ 0 -1  0] [-1  0  0], [ 0 -1  0], [-1  0  0], [ 0  0 -1], [ 0 -1  0], [ 0  0 -1] ] TESTS:: sage: orthogonal_transformation(1) [[1]] sage: orthogonal_transformation(0) Traceback (most recent call last): ... ValueError: ['B', 0] is not a valid cartan type """ if orientation_preserving: return [w.matrix() for w in WeylGroup(['B', n]) if w.matrix().det() == 1] else: return [w.matrix() for w in WeylGroup(['B', n])] ############################## # Class Polyomino ############################## class Polyomino(SageObject): r""" Return the polyomino defined by a set of coordinates. The polyomino is the union of the unit square (or cube, or n-cube) centered at those coordinates. Such an object should be connected, but the code do not make this assumption. INPUT: - coords - iterable of tuple - color - string (optional, default: 'gray'), the color EXAMPLES:: sage: from sage.combinat.tiling import Polyomino sage: Polyomino([(0,0,0), (0,1,0), (1,1,0), (1,1,1)], color='blue') Polyomino: [(0, 0, 0), (0, 1, 0), (1, 1, 0), (1, 1, 1)], Color: blue """ def __init__(self, coords, color='gray'): r""" INPUT: - coords - iterable of tuple - color - string (optional, default: 'gray'), the color EXAMPLES:: sage: from sage.combinat.tiling import Polyomino sage: Polyomino([(0,0,0), (0,1,0), (1,1,0), (1,1,1)], color='blue') Polyomino: [(0, 0, 0), (0, 1, 0), (1, 1, 0), (1, 1, 1)], Color: blue :: sage: from sage.combinat.tiling import Polyomino sage: Polyomino([(0,0), (1,0), (2,0)]) Polyomino: [(0, 0), (1, 0), (2, 0)], Color: gray """ assert isinstance(color, str) self._color = color self._blocs = frozenset(tuple(c) for c in coords) assert len(self._blocs) != 0, "Polyomino must be non empty" dimension_set = set(len(a) for a in self._blocs) assert len(dimension_set) <= 1, "coord must be all of the same dimension" self._dimension = dimension_set.pop() def __repr__(self): r""" String representation. EXAMPLES:: sage: from sage.combinat.tiling import Polyomino sage: Polyomino([(0,0,0), (0,1,0), (1,1,0), (1,1,1)], color='red') Polyomino: [(0, 0, 0), (0, 1, 0), (1, 1, 0), (1, 1, 1)], Color: red """ s = "Polyomino: %s, " % sorted(self._blocs) s += "Color: %s" % self._color return s def __hash__(self): r""" EXAMPLES:: sage: from sage.combinat.tiling import Polyomino sage: p = Polyomino([(0,0,0), (0,1,0), (1,1,0), (1,1,1)], color='blue') sage: hash(p)     # random 2059134902 """ return hash(self._blocs) def __eq__(self, other): r""" Return whether self is equal to other. INPUT: - other - a polyomino OUTPUT: boolean EXAMPLES:: sage: from sage.combinat.tiling import Polyomino sage: p = Polyomino([(0,0,0), (0,1,0), (1,1,0), (1,1,1)], color='blue') sage: q = Polyomino([(0,0,0), (0,1,0), (1,1,0), (1,1,1)], color='red') sage: p == q True sage: r = Polyomino([(0,0,0), (0,1,0), (1,1,0)], color='blue') sage: p == r False """ return self._blocs == other._blocs def __ne__(self, other): r""" Return whether self is not equal to other. INPUT: - other - a polyomino OUTPUT: boolean EXAMPLES:: sage: from sage.combinat.tiling import Polyomino sage: p = Polyomino([(0,0,0), (0,1,0), (1,1,0), (1,1,1)], color='blue') sage: q = Polyomino([(0,0,0), (0,1,0), (1,1,0), (1,1,1)], color='red') sage: p != q False sage: r = Polyomino([(0,0,0), (0,1,0), (1,1,0)], color='blue') sage: p != r True """ return self._blocs != other._blocs def __iter__(self): r""" EXAMPLES:: sage: from sage.combinat.tiling import Polyomino sage: p = Polyomino([(0,0,0), (0,1,0), (1,1,0), (1,1,1)], color='blue') sage: it = iter(p) sage: it.next() (1, 1, 0) """ return iter(self._blocs) def color(self): r""" Return the color of the polyomino. EXAMPLES:: sage: from sage.combinat.tiling import Polyomino sage: p = Polyomino([(0,0,0), (0,1,0), (1,1,0), (1,1,1)], color='blue') sage: p.color() 'blue' """ return self._color def __len__(self): r""" Return the size of the polyomino, i.e. the number of n-dimensional unit cubes. EXAMPLES:: sage: from sage.combinat.tiling import Polyomino sage: p = Polyomino([(0,0,0), (0,1,0), (1,1,0), (1,1,1)], color='blue') sage: len(p) 4 """ return len(self._blocs) def orthogonals(self, orientation_preserving=True): r""" Iterator over the images of self under orthogonal transformations. .. NOTE:: No guarantee of unicity. INPUT: - orientation_preserving - bool (optional, default: True), whether the orientation is preserved OUTPUT: iterator of Polyomino EXAMPLES:: sage: from sage.combinat.tiling import Polyomino sage: p = Polyomino([(0,0,0), (0,1,0), (1,1,0), (1,1,1)], color='blue') sage: L = list(p.orthogonals()) sage: len(L) 24 sage: L = list(p.orthogonals(False)) sage: len(L) 48 """ return (m * self for m in orthogonal_transformation(self._dimension, orientation_preserving)) def canonical_orthogonals(self, orientation_preserving=True): r""" Iterator over the image of self under orthogonal transformations where the coordinates are all positive and minimal. .. NOTE:: No guarentee of unicity. INPUT: - orientation_preserving - bool (optional, default: True), whether the orientation is preserved OUTPUT: iterator of Polyomino EXAMPLES:: sage: from sage.combinat.tiling import Polyomino sage: p = Polyomino([(0,0,0), (0,1,0), (1,1,0), (1,1,1)], color='blue') sage: L = list(p.canonical_orthogonals()) sage: len(L) 24 They might not be all different:: sage: s = set(p.canonical_orthogonals()) sage: len(s) 12 With the non orientation-preserving:: sage: s = set(p.canonical_orthogonals(False)) sage: len(s) 24 """ for q in self.orthogonals(orientation_preserving): yield q.canonical() def canonical(self): r""" Returns the translated copy of self having minimal and positive coordinates EXAMPLES:: sage: from sage.combinat.tiling import Polyomino sage: p = Polyomino([(0,0,0),(1,0,0),(1,1,0),(1,1,1),(1,2,0)], color='deeppink') sage: p Polyomino: [(0, 0, 0), (1, 0, 0), (1, 1, 0), (1, 1, 1), (1, 2, 0)], Color: deeppink sage: p.canonical() Polyomino: [(0, 0, 0), (1, 0, 0), (1, 1, 0), (1, 1, 1), (1, 2, 0)], Color: deeppink TESTS:: sage: p Polyomino: [(0, 0, 0), (1, 0, 0), (1, 1, 0), (1, 1, 1), (1, 2, 0)], Color: deeppink sage: p + (3,4,5) Polyomino: [(3, 4, 5), (4, 4, 5), (4, 5, 5), (4, 5, 6), (4, 6, 5)], Color: deeppink sage: (p + (3,4,5)).canonical() Polyomino: [(0, 0, 0), (1, 0, 0), (1, 1, 0), (1, 1, 1), (1, 2, 0)], Color: deeppink """ minxyz,maxxyz = self.bounding_box() return self - minxyz def __sub__(self, v): r""" Return a translated copy of self by the opposite of the vector v. INPUT: - v - tuple OUPUT: polyomino EXAMPLES:: sage: from sage.combinat.tiling import Polyomino sage: p = Polyomino([(0,0,0),(1,0,0),(1,1,0),(1,1,1),(1,2,0)], color='deeppink') sage: p - (2,2,2) Polyomino: [(-2, -2, -2), (-1, -2, -2), (-1, -1, -2), (-1, -1, -1), (-1, 0, -2)], Color: deeppink """ if not len(v) == self._dimension: raise ValueError, "Dimension of input vector must match the dimension of the polyomino" v = vector(v) return Polyomino([vector(p)-v for p in self], color=self._color) def __add__(self, v): r""" Return a translated copy of self by the vector v. INPUT: - v - tuple OUPUT: polyomino EXAMPLES:: sage: from sage.combinat.tiling import Polyomino sage: p = Polyomino([(0,0,0),(1,0,0),(1,1,0),(1,1,1),(1,2,0)], color='deeppink') sage: p + (2,2,2) Polyomino: [(2, 2, 2), (3, 2, 2), (3, 3, 2), (3, 3, 3), (3, 4, 2)], Color: deeppink """ if not len(v) == self._dimension: raise ValueError, "Dimension of input vector must match the dimension of the polyomino" v = vector(v) return Polyomino([vector(p)+v for p in self], color=self._color) def __rmul__(self, m): r""" Return the image of the polyomino under the application of the matrix m. INPUT: - m - square matrix, matching the dimension of self. OUPUT: Polyomino EXAMPLES:: sage: from sage.combinat.tiling import Polyomino sage: p = Polyomino([(0,0,0),(1,0,0),(1,1,0),(1,1,1),(1,2,0)], color='deeppink') sage: m = matrix(3, [1,0,0,0,1,0,0,0,1]) sage: m * p Polyomino: [(0, 0, 0), (1, 0, 0), (1, 1, 0), (1, 1, 1), (1, 2, 0)], Color: deeppink sage: m = matrix(3, [1,0,0,0,0,-1,0,1,0]) sage: m * p Polyomino: [(0, 0, 0), (1, -1, 1), (1, 0, 0), (1, 0, 1), (1, 0, 2)], Color: deeppink TESTS:: sage: m = matrix(2, [1,0,0,1]) sage: m * p Traceback (most recent call last): ... ValueError: Dimension of input matrix must match the dimension of the polyomino """ if not m.nrows() == m.ncols() == self._dimension: raise ValueError, "Dimension of input matrix must match the dimension of the polyomino" return Polyomino([m * vector(p) for p in self], color=self._color) def bounding_box(self): r""" EXAMPLES:: sage: from sage.combinat.tiling import Polyomino sage: p = Polyomino([(0,0,0),(1,0,0),(1,1,0),(1,1,1),(1,2,0)], color='deeppink') sage: p.bounding_box() [[0, 0, 0], [1, 2, 1]] """ zipped_coords = zip(*self) return [map(min, zipped_coords), map(max, zipped_coords)] def translated(self, box): r""" Returns an iterator over the translated images of self inside a box. INPUT: - box - tuple, size of the box OUTPUT: iterator of 3D polyominoes EXAMPLES:: sage: from sage.combinat.tiling import Polyomino sage: p = Polyomino([(0,0,0),(1,0,0),(1,1,0),(1,1,1),(1,2,0)], color='deeppink') sage: for t in p.translated(box=(5,8,2)): t Polyomino: [(0, 0, 0), (1, 0, 0), (1, 1, 0), (1, 1, 1), (1, 2, 0)], Color: deeppink Polyomino: [(0, 1, 0), (1, 1, 0), (1, 2, 0), (1, 2, 1), (1, 3, 0)], Color: deeppink Polyomino: [(0, 2, 0), (1, 2, 0), (1, 3, 0), (1, 3, 1), (1, 4, 0)], Color: deeppink Polyomino: [(0, 3, 0), (1, 3, 0), (1, 4, 0), (1, 4, 1), (1, 5, 0)], Color: deeppink Polyomino: [(0, 4, 0), (1, 4, 0), (1, 5, 0), (1, 5, 1), (1, 6, 0)], Color: deeppink Polyomino: [(0, 5, 0), (1, 5, 0), (1, 6, 0), (1, 6, 1), (1, 7, 0)], Color: deeppink Polyomino: [(1, 0, 0), (2, 0, 0), (2, 1, 0), (2, 1, 1), (2, 2, 0)], Color: deeppink Polyomino: [(1, 1, 0), (2, 1, 0), (2, 2, 0), (2, 2, 1), (2, 3, 0)], Color: deeppink Polyomino: [(1, 2, 0), (2, 2, 0), (2, 3, 0), (2, 3, 1), (2, 4, 0)], Color: deeppink Polyomino: [(1, 3, 0), (2, 3, 0), (2, 4, 0), (2, 4, 1), (2, 5, 0)], Color: deeppink Polyomino: [(1, 4, 0), (2, 4, 0), (2, 5, 0), (2, 5, 1), (2, 6, 0)], Color: deeppink Polyomino: [(1, 5, 0), (2, 5, 0), (2, 6, 0), (2, 6, 1), (2, 7, 0)], Color: deeppink Polyomino: [(2, 0, 0), (3, 0, 0), (3, 1, 0), (3, 1, 1), (3, 2, 0)], Color: deeppink Polyomino: [(2, 1, 0), (3, 1, 0), (3, 2, 0), (3, 2, 1), (3, 3, 0)], Color: deeppink Polyomino: [(2, 2, 0), (3, 2, 0), (3, 3, 0), (3, 3, 1), (3, 4, 0)], Color: deeppink Polyomino: [(2, 3, 0), (3, 3, 0), (3, 4, 0), (3, 4, 1), (3, 5, 0)], Color: deeppink Polyomino: [(2, 4, 0), (3, 4, 0), (3, 5, 0), (3, 5, 1), (3, 6, 0)], Color: deeppink Polyomino: [(2, 5, 0), (3, 5, 0), (3, 6, 0), (3, 6, 1), (3, 7, 0)], Color: deeppink Polyomino: [(3, 0, 0), (4, 0, 0), (4, 1, 0), (4, 1, 1), (4, 2, 0)], Color: deeppink Polyomino: [(3, 1, 0), (4, 1, 0), (4, 2, 0), (4, 2, 1), (4, 3, 0)], Color: deeppink Polyomino: [(3, 2, 0), (4, 2, 0), (4, 3, 0), (4, 3, 1), (4, 4, 0)], Color: deeppink Polyomino: [(3, 3, 0), (4, 3, 0), (4, 4, 0), (4, 4, 1), (4, 5, 0)], Color: deeppink Polyomino: [(3, 4, 0), (4, 4, 0), (4, 5, 0), (4, 5, 1), (4, 6, 0)], Color: deeppink Polyomino: [(3, 5, 0), (4, 5, 0), (4, 6, 0), (4, 6, 1), (4, 7, 0)], Color: deeppink This method is independant of the translation of the polyomino:: sage: q = Polyomino([(0,0,0), (1,0,0)]) sage: list(q.translated((2,2,1))) [Polyomino: [(0, 0, 0), (1, 0, 0)], Color: gray, Polyomino: [(0, 1, 0), (1, 1, 0)], Color: gray] sage: q = Polyomino([(34,7,-9), (35,7,-9)]) sage: list(q.translated((2,2,1))) [Polyomino: [(0, 0, 0), (1, 0, 0)], Color: gray, Polyomino: [(0, 1, 0), (1, 1, 0)], Color: gray] Inside smaller boxes:: sage: list(p.translated(box=(2,2,3))) [] sage: list(p.translated(box=(2,3,2))) [Polyomino: [(0, 0, 0), (1, 0, 0), (1, 1, 0), (1, 1, 1), (1, 2, 0)], Color: deeppink] sage: list(p.translated(box=(3,2,2))) [] sage: list(p.translated(box=(1,1,1))) [] sage: list(p.translated(box=(1,1,-1))) [] """ if not len(box) == self._dimension: raise ValueError, "Dimension of input box must match the dimension of the polyomino" minxyz,maxxyz = map(vector, self.bounding_box()) size = maxxyz - minxyz cano = self.canonical() for v in xmrange(vector(box) - vector(size), tuple): yield cano + v def translated_orthogonals(self, box, orientation_preserving=True): r""" Return the translated and rotated of self that lies in the box. INPUT: - box - tuple of size three, size of the box - orientation_preserving - bool (optional, default: True), whether the orientation is preserved EXAMPLES:: sage: from sage.combinat.tiling import Polyomino sage: p = Polyomino([(0,0,0),(1,0,0),(1,1,0),(1,1,1),(1,2,0)], color='deeppink') sage: L = list(p.translated_orthogonals(box=(5,8,2))) sage: len(L) 360 :: sage: p = Polyomino([(0,0,0),(1,0,0),(1,1,0),(1,2,0),(1,2,1)], color='orange') sage: L = list(p.translated_orthogonals(box=(5,8,2))) sage: len(L) 180 :: sage: p = Polyomino([(0,0,0),(1,0,0),(1,1,0),(1,2,0),(1,2,1)], color='orange') sage: L = list(p.translated_orthogonals((5,8,2), False)) sage: len(L) 360 """ if not len(box) == self._dimension: raise ValueError, "Dimension of input box must match the dimension of the polyomino" all_distinct_cano = set(self.canonical_orthogonals(orientation_preserving)) for cano in all_distinct_cano: for t in cano.translated(box=box): yield t def neighbor_edges(self): r""" Return an iterator over the pairs of neighbor coordinates of the polyomino. Two points P and Q are neighbor if P - Q has one coordinate equal to +1 or -1 and zero everywhere else. EXAMPLES:: sage: from sage.combinat.tiling import Polyomino sage: p = Polyomino([(0,0,0),(0,0,1)]) sage: list(sorted(edge) for edge in p.neighbor_edges()) [[(0, 0, 0), (0, 0, 1)]] In 3d:: sage: p = Polyomino([(0,0,0),(1,0,0),(1,1,0),(1,1,1),(1,2,0)], color='deeppink') sage: L = sorted(sorted(edge) for edge in p.neighbor_edges()) sage: for a in L: a [(0, 0, 0), (1, 0, 0)] [(1, 0, 0), (1, 1, 0)] [(1, 1, 0), (1, 1, 1)] [(1, 1, 0), (1, 2, 0)] In 2d:: sage: p = Polyomino([(0,0),(1,0),(1,1),(1,2)]) sage: L = sorted(sorted(edge) for edge in p.neighbor_edges()) sage: for a in L: a [(0, 0), (1, 0)] [(1, 0), (1, 1)] [(1, 1), (1, 2)] """ for P, Q in itertools.combinations(self, 2): P, Q = vector(P), vector(Q) s = sorted(map(abs, Q-P)) firsts = s[:-1] last = s[-1] if last == 1 and all(f == 0 for f in firsts): yield P, Q def center(self): r""" Return the center of the polyomino. EXAMPLES:: sage: from sage.combinat.tiling import Polyomino sage: p = Polyomino([(0,0,0),(0,0,1)]) sage: p.center() (0, 0, 1/2) In 3d:: sage: p = Polyomino([(0,0,0),(1,0,0),(1,1,0),(1,1,1),(1,2,0)], color='deeppink') sage: p.center() (4/5, 4/5, 1/5) In 2d:: sage: p = Polyomino([(0,0),(1,0),(1,1),(1,2)]) sage: p.center() (3/4, 3/4) """ return sum(vector(t) for t in self) / len(self) def boundary(self): r""" Return the boundary of a 2D polyomino. INPUT: - self - a 2D polyomino OUTPUT: - list of edges (an edge is a pair of adjacent 2D coordinates) EXAMPLES:: sage: from sage.combinat.tiling import Polyomino sage: p = Polyomino([(0,0), (1,0), (0,1), (1,1)]) sage: p.boundary() [((0.5, 1.5), (1.5, 1.5)), ((-0.5, -0.5), (0.5, -0.5)), ((0.5, -0.5), (1.5, -0.5)), ((-0.5, 1.5), (0.5, 1.5)), ((-0.5, 0.5), (-0.5, 1.5)), ((-0.5, -0.5), (-0.5, 0.5)), ((1.5, 0.5), (1.5, 1.5)), ((1.5, -0.5), (1.5, 0.5))] sage: len(_) 8 sage: p = Polyomino([(5,5)]) sage: p.boundary() [((4.5, 5.5), (5.5, 5.5)), ((4.5, 4.5), (5.5, 4.5)), ((4.5, 4.5), (4.5, 5.5)), ((5.5, 4.5), (5.5, 5.5))] """ if self._dimension != 2: raise NotImplementedError("The method boundary is currently implemented " "only for dimension 2") from collections import defaultdict horizontal = defaultdict(int) vertical = defaultdict(int) for a in self: x,y = a = tuple(a) horizontal[a] += 1 vertical[a] += 1 horizontal[(x,y+1)] -= 1 vertical[(x+1,y)] -= 1 edges = [] h = 0.5 for (x,y), coeff in horizontal.iteritems(): if coeff != 0: edges.append(((x-h,y-h),(x+h,y-h))) for (x,y), coeff in vertical.iteritems(): if coeff != 0: edges.append(((x-h,y-h),(x-h,y+h))) return edges def show3d(self, size=1): r""" Returns a 3d Graphic object representing the polyomino. INPUT: - self - a polyomino of dimension 3 - size - number (optional, default: 1), the size of each 1 \times 1 \times 1 cube. This does a homothety with respect to the center of the polyomino. EXAMPLES:: sage: from sage.combinat.tiling import Polyomino sage: p = Polyomino([(0,0,0), (0,1,0), (1,1,0), (1,1,1)], color='blue') sage: p.show3d() """ assert self._dimension == 3, "Dimension of the polyomino must be 3." G = Graphics() for p in self: G += cube(p, color=self._color) center = self.center() G = G.translate(-center) G = G.scale(size) G = G.translate(center) return G def show2d(self, size=0.7, color='black', thickness=1): r""" Returns a 2d Graphic object representing the polyomino. INPUT: - self - a polyomino of dimension 2 - size - number (optional, default: 0.7), the size of each square. - color - color (optional, default: 'black'), color of the boundary line. - thickness - number (optional, default: 1), how thick the boundary line is. EXAMPLES:: sage: from sage.combinat.tiling import Polyomino sage: p = Polyomino([(0,0),(1,0),(1,1),(1,2)], color='deeppink') sage: p.show2d()              # long time (0.5s) """ assert self._dimension == 2, "Dimension of the polyomino must be 2." h = size / 2.0 G = Graphics() for a,b in self: G += circle((a,b), h, fill=True, color=self._color) k = h / 2.0 for P,Q in self.neighbor_edges(): a,b = (P + Q) / 2.0 G += polygon([(a-k,b-k), (a+k,b-k), (a+k,b+k), (a-k,b+k), (a-k,b-k)], color=self._color) for edge in self.boundary(): G += line(edge, color=color, thickness=thickness) return G ####################### # General tiling solver ####################### class TilingSolver(SageObject): r""" Tiling solver Solve the problem of tiling a rectangular box with a certain number of pieces, called polyominoes, where each polyomino must be used exactly once. INPUT: - pieces - iterable of Polyominoes - box - tuple, size of the box - rotation - bool (optional, default: True), whether to allow rotations - reflection - bool (optional, default: False), whether to allow reflections - reusable - bool (optional, default: False), whether to allow the pieces to be reused EXAMPLES: By default, rotations are allowed and reflections are not allowed:: sage: from sage.combinat.tiling import TilingSolver, Polyomino sage: p = Polyomino([(0,0,0)]) sage: q = Polyomino([(0,0,0), (0,0,1)]) sage: r = Polyomino([(0,0,0), (0,0,1), (0,0,2)]) sage: T = TilingSolver([p,q,r], box=(1,1,6)) sage: T Tiling solver of 3 pieces into the box (1, 1, 6) Rotation allowed: True Reflection allowed: False Reusing pieces allowed: False Solutions are given by an iterator:: sage: it = T.solve() sage: for p in it.next(): p Polyomino: [(0, 0, 0)], Color: gray Polyomino: [(0, 0, 1), (0, 0, 2)], Color: gray Polyomino: [(0, 0, 3), (0, 0, 4), (0, 0, 5)], Color: gray Another solution:: sage: for p in it.next(): p Polyomino: [(0, 0, 0)], Color: gray Polyomino: [(0, 0, 1), (0, 0, 2), (0, 0, 3)], Color: gray Polyomino: [(0, 0, 4), (0, 0, 5)], Color: gray TESTS:: sage: T = TilingSolver([p,q,r], box=(1,1,6), rotation=False, reflection=True) Traceback (most recent call last): ... NotImplementedError: When reflection is allowed and rotation is not allowed """ def __init__(self, pieces, box, rotation=True, reflection=False, reusable=False): r""" Constructor. EXAMPLES:: sage: from sage.combinat.tiling import TilingSolver, Polyomino sage: p = Polyomino([(0,0,0)]) sage: q = Polyomino([(0,0,0), (0,0,1)]) sage: r = Polyomino([(0,0,0), (0,0,1), (0,0,2)]) sage: T = TilingSolver([p,q,r], box=(1,1,6)) sage: T Tiling solver of 3 pieces into the box (1, 1, 6) Rotation allowed: True Reflection allowed: False Reusing pieces allowed: False """ self._pieces = pieces self._box = box self._rotation = rotation self._reflection = reflection if not self._rotation and self._reflection: raise NotImplementedError, "When reflection is allowed and rotation is not allowed" self._reusable = reusable self._starting_rows = None    # the starting row of each piece def __repr__(self): r""" String representation EXAMPLES:: sage: from sage.combinat.tiling import TilingSolver, Polyomino sage: p = Polyomino([(0,0,0)]) sage: q = Polyomino([(0,0,0), (0,0,1)]) sage: r = Polyomino([(0,0,0), (0,0,1), (0,0,2)]) sage: TilingSolver([p,q,r], box=(1,1,6)) Tiling solver of 3 pieces into the box (1, 1, 6) Rotation allowed: True Reflection allowed: False Reusing pieces allowed: False """ N = len(self._pieces) s = "Tiling solver of %s pieces into the box %s\n" % (N, self._box) s += "Rotation allowed: %s\n" % self._rotation s += "Reflection allowed: %s\n" % self._reflection s += "Reusing pieces allowed: %s" % self._reusable return s def is_suitable(self): r""" Return whether the volume of the box is equal to sum of the volume of the polyominoes and the number of rows sent to the DLX solver is larger than zero. If these conditions are not verified, then the problem is not suitable in the sense that there are no solution. .. NOTE:: The DLX solver throws a Segmentation Fault when the number of rows is zero:: sage: from sage.combinat.matrices.dancing_links import dlx_solver sage: rows = [] sage: x = dlx_solver(rows) sage: x.search()        # not tested BOOM !!! EXAMPLES:: sage: from sage.combinat.tiling import TilingSolver, Polyomino sage: p = Polyomino([(0,0,0)]) sage: q = Polyomino([(0,0,0), (0,0,1)]) sage: r = Polyomino([(0,0,0), (0,0,1), (0,0,2)]) sage: T = TilingSolver([p,q,r], box=(1,1,6)) sage: T.is_suitable() True sage: T = TilingSolver([p,q,r], box=(1,1,7)) sage: T.is_suitable() False """ if self._reusable: return len(self.rows()) != 0 else: return (sum(len(p) for p in self.pieces()) == prod(self._box) and len(self.rows()) != 0) def pieces(self): r""" Return the list of pieces. OUTPUT: list of 3D polyominoes EXAMPLES:: sage: from sage.combinat.tiling import TilingSolver, Polyomino sage: p = Polyomino([(0,0,0)]) sage: q = Polyomino([(0,0,0), (0,0,1)]) sage: r = Polyomino([(0,0,0), (0,0,1), (0,0,2)]) sage: T = TilingSolver([p,q,r], box=(1,1,6)) sage: for p in T._pieces: p Polyomino: [(0, 0, 0)], Color: gray Polyomino: [(0, 0, 0), (0, 0, 1)], Color: gray Polyomino: [(0, 0, 0), (0, 0, 1), (0, 0, 2)], Color: gray """ return self._pieces def space(self): r""" Returns an iterator over all the non negative integer coordinates contained in the box. EXAMPLES:: sage: from sage.combinat.tiling import TilingSolver, Polyomino sage: p = Polyomino([(0,0,0)]) sage: q = Polyomino([(0,0,0), (0,0,1)]) sage: r = Polyomino([(0,0,0), (0,0,1), (0,0,2)]) sage: T = TilingSolver([p,q,r], box=(1,1,6)) sage: list(T.space()) [(0, 0, 0), (0, 0, 1), (0, 0, 2), (0, 0, 3), (0, 0, 4), (0, 0, 5)] """ return xmrange(self._box, tuple) @cached_method def coord_to_int_dict(self): r""" Returns a dictionary mapping coordinates to integers. OUTPUT: dict EXAMPLES:: sage: from sage.combinat.tiling import TilingSolver, Polyomino sage: p = Polyomino([(0,0,0)]) sage: q = Polyomino([(0,0,0), (0,0,1)]) sage: r = Polyomino([(0,0,0), (0,0,1), (0,0,2)]) sage: T = TilingSolver([p,q,r], box=(1,1,6)) sage: A = T.coord_to_int_dict() sage: sorted(A.iteritems()) [((0, 0, 0), 3), ((0, 0, 1), 4), ((0, 0, 2), 5), ((0, 0, 3), 6), ((0, 0, 4), 7), ((0, 0, 5), 8)] Reusable pieces:: sage: p = Polyomino([(0,0), (0,1)]) sage: q = Polyomino([(0,0), (0,1), (1,0), (1,1)]) sage: T = TilingSolver([p,q], box=[3,2], reusable=True) sage: B = T.coord_to_int_dict() sage: sorted(B.iteritems()) [((0, 0), 0), ((0, 1), 1), ((1, 0), 2), ((1, 1), 3), ((2, 0), 4), ((2, 1), 5)] """ if self._reusable: return dict( (c,i) for i,c in enumerate(self.space()) ) else: number_of_pieces = len(self._pieces) return dict( (c,i+number_of_pieces) for i,c in enumerate(self.space()) ) @cached_method def int_to_coord_dict(self): r""" Returns a dictionary mapping integers to coordinates. EXAMPLES:: sage: from sage.combinat.tiling import TilingSolver, Polyomino sage: p = Polyomino([(0,0,0)]) sage: q = Polyomino([(0,0,0), (0,0,1)]) sage: r = Polyomino([(0,0,0), (0,0,1), (0,0,2)]) sage: T = TilingSolver([p,q,r], box=(1,1,6)) sage: B = T.int_to_coord_dict() sage: sorted(B.iteritems()) [(3, (0, 0, 0)), (4, (0, 0, 1)), (5, (0, 0, 2)), (6, (0, 0, 3)), (7, (0, 0, 4)), (8, (0, 0, 5))] Reusable pieces:: sage: from sage.combinat.tiling import Polyomino, TilingSolver sage: p = Polyomino([(0,0), (0,1)]) sage: q = Polyomino([(0,0), (0,1), (1,0), (1,1)]) sage: T = TilingSolver([p,q], box=[3,2], reusable=True) sage: B = T.int_to_coord_dict() sage: sorted(B.iteritems()) [(0, (0, 0)), (1, (0, 1)), (2, (1, 0)), (3, (1, 1)), (4, (2, 0)), (5, (2, 1))] TESTS: The methods int_to_coord_dict and coord_to_int_dict returns dictionary that are inverse of each other:: sage: A = T.coord_to_int_dict() sage: B = T.int_to_coord_dict() sage: all(A[B[i]] == i for i in B) True sage: all(B[A[i]] == i for i in A) True """ if self._reusable: return dict( (i,c) for i,c in enumerate(self.space()) ) else: number_of_pieces = len(self._pieces) return dict( (i+number_of_pieces,c) for i,c in enumerate(self.space()) ) @cached_method def rows(self): r""" Creation of the rows EXAMPLES:: sage: from sage.combinat.tiling import TilingSolver, Polyomino sage: p = Polyomino([(0,0,0)]) sage: q = Polyomino([(0,0,0), (0,0,1)]) sage: r = Polyomino([(0,0,0), (0,0,1), (0,0,2)]) sage: T = TilingSolver([p,q,r], box=(1,1,6)) sage: rows = T.rows() sage: for row in rows: row [0, 3] [0, 4] [0, 5] [0, 6] [0, 7] [0, 8] [1, 3, 4] [1, 4, 5] [1, 5, 6] [1, 6, 7] [1, 8, 7] [2, 3, 4, 5] [2, 4, 5, 6] [2, 5, 6, 7] [2, 8, 6, 7] """ coord_to_int = self.coord_to_int_dict() rows = [] self._starting_rows = [] # indices of the first row for each piece for i,p in enumerate(self._pieces): self._starting_rows.append(len(rows)) if self._rotation and self._reflection: it = p.translated_orthogonals(self._box, orientation_preserving=False) elif self._rotation and not self._reflection: it = p.translated_orthogonals(self._box, orientation_preserving=True) elif not self._rotation and self._reflection: raise NotImplementedError, "Reflection allowed, Rotation not allowed is not implemented" else: it = p.translated(self._box) if self._reusable: for q in it: rows.append([coord_to_int[coord] for coord in q]) else: for q in it: rows.append([i] + [coord_to_int[coord] for coord in q]) self._starting_rows.append(len(rows)) return rows def nrows_per_piece(self): r""" Return the number of rows necessary by each piece. OUPUT: list EXAMPLES:: sage: from sage.games.quantumino import QuantuminoSolver sage: q = QuantuminoSolver(0) sage: T = q.tiling_solver() sage: T.nrows_per_piece()                           # long time (10s) [360, 360, 360, 360, 360, 180, 180, 672, 672, 360, 360, 180, 180, 360, 360, 180] """ if self._starting_rows is None: rows = self.rows() L = self._starting_rows return [L[i+1] - L[i] for i in xrange(len(L)-1)] def dlx_solver(self): r""" Return the sage DLX solver of that 3D tiling problem. OUTPUT: DLX Solver EXAMPLES:: sage: from sage.combinat.tiling import TilingSolver, Polyomino sage: p = Polyomino([(0,0,0)]) sage: q = Polyomino([(0,0,0), (0,0,1)]) sage: r = Polyomino([(0,0,0), (0,0,1), (0,0,2)]) sage: T = TilingSolver([p,q,r], box=(1,1,6)) sage: x = T.dlx_solver() sage: x """ from sage.combinat.matrices.dancing_links import dlx_solver rows = self.rows() assert len(rows) != 0, "Number of rows given to the DLX solver must not be zero" x = dlx_solver(rows) return x def dlx_solutions(self): r""" Return an iterator over the row indices of the solutions. OUPUT: iterator EXAMPLES:: sage: from sage.combinat.tiling import TilingSolver, Polyomino sage: p = Polyomino([(0,0,0)]) sage: q = Polyomino([(0,0,0), (0,0,1)]) sage: r = Polyomino([(0,0,0), (0,0,1), (0,0,2)]) sage: T = TilingSolver([p,q,r], box=(1,1,6)) sage: list(T.dlx_solutions()) [[0, 7, 14], [0, 12, 10], [6, 13, 5], [6, 14, 2], [11, 9, 5], [11, 10, 3]] """ if len(self.rows()) == 0: raise StopIteration x = self.dlx_solver() while x.search() == 1: yield x.get_solution() def dlx_common_prefix_solutions(self): r""" Return an iterator over the row indices of solutions and of partial solutions, i.e. the common prefix of two consecutive solutions. The purpose is to illustrate the backtracking and construct an animation of the evolution of solutions. OUPUT: iterator EXAMPLES:: sage: from sage.combinat.tiling import TilingSolver, Polyomino sage: p = Polyomino([(0,0,0)]) sage: q = Polyomino([(0,0,0), (0,0,1)]) sage: r = Polyomino([(0,0,0), (0,0,1), (0,0,2)]) sage: T = TilingSolver([p,q,r], box=(1,1,6)) sage: list(T.dlx_solutions()) [[0, 7, 14], [0, 12, 10], [6, 13, 5], [6, 14, 2], [11, 9, 5], [11, 10, 3]] sage: list(T.dlx_common_prefix_solutions()) [[0, 7, 14], [0], [0, 12, 10], [], [6, 13, 5], [6], [6, 14, 2], [], [11, 9, 5], [11], [11, 10, 3]] :: sage: from sage.combinat.tiling import TilingSolver, Polyomino sage: y = Polyomino([(0,0),(1,0),(2,0),(3,0),(2,1)], color='yellow') sage: T = TilingSolver([y], box=(5,10), reusable=True, reflection=True) sage: for a in T.dlx_common_prefix_solutions(): a [18, 119, 68, 33, 48, 23, 8, 73, 58, 43] [18, 119, 68, 33, 48] [18, 119, 68, 33, 48, 133, 8, 73, 168, 43] [18, 119] [18, 119, 178, 143, 48, 23, 8, 73, 58, 43] [18, 119, 178, 143, 48] [18, 119, 178, 143, 48, 133, 8, 73, 168, 43] [18, 119, 178] [18, 119, 178, 164, 152, 73, 8, 133, 159, 147] [] [74, 19, 177, 33, 49, 134, 109, 72, 58, 42] [74, 19, 177] [74, 19, 177, 54, 151, 72, 109, 134, 160, 37] [74, 19, 177, 54, 151] [74, 19, 177, 54, 151, 182, 109, 134, 160, 147] [74] [74, 129, 177, 164, 151, 72, 109, 134, 160, 37] [74, 129, 177, 164, 151] [74, 129, 177, 164, 151, 182, 109, 134, 160, 147] """ it = self.dlx_solutions() B = it.next() while True: yield B A, B = B, it.next() common_prefix = [] for a,b in itertools.izip(A,B): if a == b: common_prefix.append(a) else: break yield common_prefix def dlx_incremental_solutions(self): r""" Return an iterator over the row indices of the incremental solutions. Between two incremental solution, either one piece is added or one piece is removed. The purpose is to illustrate the backtracking and construct an animation of the evolution of solutions. OUPUT: iterator EXAMPLES:: sage: from sage.combinat.tiling import TilingSolver, Polyomino sage: p = Polyomino([(0,0,0)]) sage: q = Polyomino([(0,0,0), (0,0,1)]) sage: r = Polyomino([(0,0,0), (0,0,1), (0,0,2)]) sage: T = TilingSolver([p,q,r], box=(1,1,6)) sage: list(T.dlx_solutions()) [[0, 7, 14], [0, 12, 10], [6, 13, 5], [6, 14, 2], [11, 9, 5], [11, 10, 3]] sage: list(T.dlx_incremental_solutions()) [[0, 7, 14], [0, 7], [0], [0, 12], [0, 12, 10], [0, 12], [0], [], [6], [6, 13], [6, 13, 5], [6, 13], [6], [6, 14], [6, 14, 2], [6, 14], [6], [], [11], [11, 9], [11, 9, 5], [11, 9], [11], [11, 10], [11, 10, 3]] :: sage: y = Polyomino([(0,0),(1,0),(2,0),(3,0),(2,1)], color='yellow') sage: T = TilingSolver([y], box=(5,10), reusable=True, reflection=True) sage: for a in T.dlx_solutions(): a [18, 119, 68, 33, 48, 23, 8, 73, 58, 43] [18, 119, 68, 33, 48, 133, 8, 73, 168, 43] [18, 119, 178, 143, 48, 23, 8, 73, 58, 43] [18, 119, 178, 143, 48, 133, 8, 73, 168, 43] [18, 119, 178, 164, 152, 73, 8, 133, 159, 147] [74, 19, 177, 33, 49, 134, 109, 72, 58, 42] [74, 19, 177, 54, 151, 72, 109, 134, 160, 37] [74, 19, 177, 54, 151, 182, 109, 134, 160, 147] [74, 129, 177, 164, 151, 72, 109, 134, 160, 37] [74, 129, 177, 164, 151, 182, 109, 134, 160, 147] sage: len(list(T.dlx_incremental_solutions())) 123 """ it = self.dlx_solutions() B = it.next() while True: yield B A, B = B, it.next() common_prefix = 0 for a,b in itertools.izip(A,B): if a == b: common_prefix += 1 else: break for i in xrange(1,len(A)-common_prefix): yield A[:-i] for j in xrange(common_prefix, len(B)): yield B[:j] def solve(self, partial=None): r""" Returns an iterator of list of 3D polyominoes that are an exact cover of the box. INPUT: - partial - string (optional, default: None), whether to include partial (incomplete) solutions. It can be one of the following: - None - include only complete solution - 'common_prefix' - common prefix between two consecutive solutions - 'incremental' - one piece change at a time OUTPUT: iterator of list of 3D polyominoes EXAMPLES:: sage: from sage.combinat.tiling import TilingSolver, Polyomino sage: p = Polyomino([(0,0,0)]) sage: q = Polyomino([(0,0,0), (0,0,1)]) sage: r = Polyomino([(0,0,0), (0,0,1), (0,0,2)]) sage: T = TilingSolver([p,q,r], box=(1,1,6)) sage: it = T.solve() sage: for p in it.next(): p Polyomino: [(0, 0, 0)], Color: gray Polyomino: [(0, 0, 1), (0, 0, 2)], Color: gray Polyomino: [(0, 0, 3), (0, 0, 4), (0, 0, 5)], Color: gray sage: for p in it.next(): p Polyomino: [(0, 0, 0)], Color: gray Polyomino: [(0, 0, 1), (0, 0, 2), (0, 0, 3)], Color: gray Polyomino: [(0, 0, 4), (0, 0, 5)], Color: gray sage: for p in it.next(): p Polyomino: [(0, 0, 0), (0, 0, 1)], Color: gray Polyomino: [(0, 0, 2), (0, 0, 3), (0, 0, 4)], Color: gray Polyomino: [(0, 0, 5)], Color: gray Including the partial solutions:: sage: it = T.solve(partial='common_prefix') sage: for p in it.next(): p Polyomino: [(0, 0, 0)], Color: gray Polyomino: [(0, 0, 1), (0, 0, 2)], Color: gray Polyomino: [(0, 0, 3), (0, 0, 4), (0, 0, 5)], Color: gray sage: for p in it.next(): p Polyomino: [(0, 0, 0)], Color: gray sage: for p in it.next(): p Polyomino: [(0, 0, 0)], Color: gray Polyomino: [(0, 0, 1), (0, 0, 2), (0, 0, 3)], Color: gray Polyomino: [(0, 0, 4), (0, 0, 5)], Color: gray sage: for p in it.next(): p sage: for p in it.next(): p Polyomino: [(0, 0, 0), (0, 0, 1)], Color: gray Polyomino: [(0, 0, 2), (0, 0, 3), (0, 0, 4)], Color: gray Polyomino: [(0, 0, 5)], Color: gray Colors are preserved when the polyomino can be reused:: sage: p = Polyomino([(0,0,0)], color='yellow') sage: q = Polyomino([(0,0,0), (0,0,1)], color='yellow') sage: r = Polyomino([(0,0,0), (0,0,1), (0,0,2)], color='yellow') sage: T = TilingSolver([p,q,r], box=(1,1,6), reusable=True) sage: it = T.solve() sage: for p in it.next(): p Polyomino: [(0, 0, 0)], Color: yellow Polyomino: [(0, 0, 1)], Color: yellow Polyomino: [(0, 0, 2)], Color: yellow Polyomino: [(0, 0, 3)], Color: yellow Polyomino: [(0, 0, 4)], Color: yellow Polyomino: [(0, 0, 5)], Color: yellow TESTS:: sage: T = TilingSolver([p,q,r], box=(1,1,7)) sage: T.solve().next() Traceback (most recent call last): ... StopIteration """ if not self.is_suitable(): raise StopIteration int_to_coord = self.int_to_coord_dict() rows = self.rows() if partial is None: it = self.dlx_solutions() elif partial == 'common_prefix': it = self.dlx_common_prefix_solutions() elif partial == 'incremental': it = self.dlx_incremental_solutions() else: raise ValueError("Unknown value for partial (=%s)" % partial) for solution in it: pieces = [] for row_number in solution: row = rows[row_number] if self._reusable: no = -1 while self._starting_rows[no] < row_number: no += 1 coords = [int_to_coord[i] for i in row] p = Polyomino(coords, color=self._pieces[no].color()) else: no = row[0] coords = [int_to_coord[i] for i in row[1:]] p = Polyomino(coords, color=self._pieces[no].color()) pieces.append(p) yield pieces def number_of_solutions(self): r""" Return the number of distinct solutions. OUPUT: integer EXAMPLES:: sage: from sage.combinat.tiling import TilingSolver, Polyomino sage: p = Polyomino([(0,0)]) sage: q = Polyomino([(0,0), (0,1)]) sage: r = Polyomino([(0,0), (0,1), (0,2)]) sage: T = TilingSolver([p,q,r], box=(1,6)) sage: T.number_of_solutions() 6 :: sage: T = TilingSolver([p,q,r], box=(1,7)) sage: T.number_of_solutions() 0 """ if not self.is_suitable(): return 0 x = self.dlx_solver() N = 0 while x.search() == 1: N += 1 return N def animate(self, partial=None, stop=None, size=0.75, axes=False): r""" Return an animation of evolving solutions. INPUT: - partial - string (optional, default: None), whether to include partial (incomplete) solutions. It can be one of the following: - None - include only complete solutions - 'common_prefix' - common prefix between two consecutive solutions - 'incremental' - one piece change at a time - stop - integer (optional, default:None), number of frames - size - number (optional, default: 0.75), the size of each 1 \times 1 square. This does a homothety with respect to the center of each polyomino. - axes - bool (optional, default:False), whether the x and y axes are shown. EXAMPLES:: sage: from sage.combinat.tiling import Polyomino, TilingSolver sage: y = Polyomino([(0,0),(1,0),(2,0),(3,0),(2,1)], color='cyan') sage: T = TilingSolver([y], box=(5,10), reusable=True, reflection=True) sage: a = T.animate() sage: a Animation with 10 frames Include partial solutions (common prefix between two consecutive solutions):: sage: a = T.animate('common_prefix') sage: a Animation with 19 frames Incremental solutions (one piece removed or added at a time):: sage: a = T.animate('incremental')      # long time (2s) sage: a                                 # long time (2s) Animation with 123 frames :: sage: a.show()                 # optional - requires convert command The show function takes arguments to specify the delay between frames (measured in hundredths of a second, default value 20) and the number of iterations (default value 0, which means to iterate forever). To iterate 4 times with half a second between each frame:: sage: a.show(delay=50, iterations=4) # optional Limit the number of frames:: sage: a = T.animate('incremental', stop=13)     # not tested sage: a                                         # not tested Animation with 13 frames """ dimension = len(self._box) if dimension == 2: it = self.solve(partial=partial) it = itertools.islice(it, stop) L = [sum([piece.show2d(size) for piece in solution], Graphics()) for solution in it] xmax, ymax = self._box xmax = xmax-0.5 ymax = ymax-0.5 a = Animation(L, xmin=-0.5, ymin=-0.5, xmax=xmax, ymax=ymax, aspect_ratio=1, axes=axes) return a elif dimension == 3: raise NotImplementedError("3d Animation must be implemented in Jmol first") else: raise NotImplementedError("Dimension must be 2 or 3 in order to make an animation")
• ## new file sage/games/quantumino.py

diff --git a/sage/games/quantumino.py b/sage/games/quantumino.py
new file mode 100644
 - # coding=utf-8 r""" Family Games America's Quantumino solver This module allows to solve the Quantumino puzzle _ made by Family Games America (see also this video _ on Youtube). This puzzle was left at the dinner room of the Laboratoire de Combinatoire Informatique Mathematique in Montreal by Franco Saliola during winter 2011. The solution uses the dancing links code which is in Sage and is based on the more general code available in the module :mod:sage.combinat.tiling. Dancing links were originally introduced by Donald Knuth in 2000 (arXiv:cs/0011047 _). In particular, Knuth used dancing links to solve tilings of a region by 2D pentaminos. Here we extend the method for 3D pentaminos. This module defines two classes : - :class:sage.games.quantumino.QuantuminoState class, to represent a state of the Quantumino game, i.e. a solution or a partial solution. - :class:sage.games.quantumino.QuantuminoSolver class, to find, enumerate and count the number of solutions of the Quantumino game where one of the piece is put aside. AUTHOR: - Sebastien Labbe, April 28th, 2011 DESCRIPTION (from [1]): " Pentamino games have been taken to a whole different level; a 3-D level, with this colorful creation! Using the original pentamino arrangements of 5 connected squares which date from 1907, players are encouraged to "think inside the box" as they try to fit 16 of the 17 3-D pentamino pieces inside the playing perimeters. Remove a different piece each time you play for an entirely new challenge! Thousands of solutions to be found! Quantumino hands-on educational tool where players learn how shapes can be transformed or arranged into predefined shapes and spaces. Includes: 1 wooden frame, 17 wooden blocks, instruction booklet. Age: 8+ " EXAMPLES: Here are the 17 wooden blocks of the Quantumino puzzle numbered from 0 to 16 in the following 3d picture. They will show up in 3D in your default (=Jmol) viewer:: sage: from sage.games.quantumino import show_pentaminos sage: show_pentaminos() To solve the puzzle where the pentamino numbered 12 is put aside:: sage: from sage.games.quantumino import QuantuminoSolver sage: s = QuantuminoSolver(12).solve().next()         # long time (10 s) sage: s                                               # long time (<1s) Quantumino state where the following pentamino is put aside : Polyomino: [(0, 0, 0), (1, 0, 0), (1, 1, 0), (1, 1, 1), (2, 1, 1)], Color: blue sage: s.show3d()                                      # long time (<1s) To remove the frame:: sage: s.show3d().show(frame=False)                    # long time (<1s) To solve the puzzle where the pentamino numbered 7 is put aside:: sage: s = QuantuminoSolver(7).solve().next()          # long time (10 s) sage: s                                               # long time (<1s) Quantumino state where the following pentamino is put aside : Polyomino: [(0, 0, 0), (0, 1, 0), (0, 2, 0), (0, 2, 1), (1, 0, 0)], Color: orange sage: s.show3d()                                      # long time (<1s) The solution is iterable. This may be used to explicitly list the positions of each pentamino:: sage: for p in s: p                                   # long time (<1s) Polyomino: [(0, 0, 0), (1, 0, 0), (1, 1, 0), (1, 1, 1), (1, 2, 0)], Color: deeppink Polyomino: [(0, 0, 1), (0, 1, 0), (0, 1, 1), (0, 2, 1), (1, 2, 1)], Color: deeppink Polyomino: [(0, 2, 0), (0, 3, 0), (0, 4, 0), (1, 4, 0), (1, 4, 1)], Color: green Polyomino: [(0, 3, 1), (1, 3, 1), (2, 2, 0), (2, 2, 1), (2, 3, 1)], Color: green Polyomino: [(1, 3, 0), (2, 3, 0), (2, 4, 0), (2, 4, 1), (3, 4, 0)], Color: red Polyomino: [(1, 0, 1), (2, 0, 1), (2, 1, 0), (2, 1, 1), (3, 1, 1)], Color: red Polyomino: [(2, 0, 0), (3, 0, 0), (3, 0, 1), (3, 1, 0), (4, 0, 0)], Color: gray Polyomino: [(3, 2, 0), (4, 0, 1), (4, 1, 0), (4, 1, 1), (4, 2, 0)], Color: purple Polyomino: [(3, 2, 1), (3, 3, 0), (3, 3, 1), (4, 2, 1), (4, 3, 1)], Color: yellow Polyomino: [(3, 4, 1), (3, 5, 1), (4, 3, 0), (4, 4, 0), (4, 4, 1)], Color: blue Polyomino: [(0, 4, 1), (0, 5, 0), (0, 5, 1), (0, 6, 1), (1, 5, 0)], Color: midnightblue Polyomino: [(0, 6, 0), (0, 7, 0), (0, 7, 1), (1, 7, 0), (2, 7, 0)], Color: darkblue Polyomino: [(1, 7, 1), (2, 6, 0), (2, 6, 1), (2, 7, 1), (3, 6, 0)], Color: blue Polyomino: [(1, 5, 1), (1, 6, 0), (1, 6, 1), (2, 5, 0), (2, 5, 1)], Color: yellow Polyomino: [(3, 6, 1), (3, 7, 0), (3, 7, 1), (4, 5, 1), (4, 6, 1)], Color: purple Polyomino: [(3, 5, 0), (4, 5, 0), (4, 6, 0), (4, 7, 0), (4, 7, 1)], Color: orange To get all the solutions, use the iterator returned by the solve method. Note that finding the first solution is the most time consuming because it needs to create the complete data to describe the problem:: sage: it = QuantuminoSolver(7).solve() sage: it.next()                                     # not tested (10s) Quantumino state where the following pentamino is put aside : Polyomino: [(0, 0, 0), (0, 1, 0), (0, 2, 0), (0, 2, 1), (1, 0, 0)], Color: orange sage: it.next()                                     # not tested (0.001s) Quantumino state where the following pentamino is put aside : Polyomino: [(0, 0, 0), (0, 1, 0), (0, 2, 0), (0, 2, 1), (1, 0, 0)], Color: orange sage: it.next()                                     # not tested (0.001s) Quantumino state where the following pentamino is put aside : Polyomino: [(0, 0, 0), (0, 1, 0), (0, 2, 0), (0, 2, 1), (1, 0, 0)], Color: orange To get the solution inside other boxes:: sage: s = QuantuminoSolver(7, box=(4,4,5)).solve().next()       # not tested (2s) sage: s.show3d()                                                # not tested (<1s) :: sage: s = QuantuminoSolver(7, box=(2,2,20)).solve().next()      # not tested (1s) sage: s.show3d()                                                # not tested (<1s) If there are no solution, a StopIteration error is raised:: sage: QuantuminoSolver(7, box=(3,3,3)).solve().next() Traceback (most recent call last): ... StopIteration The implementation allows a lot of introspection. From the :class:~sage.combinat.tiling.TilingSolver object, it is possible to retrieve the rows that are passed to the DLX solver and count them. It is also possible to get an instance of the DLX solver to play with it:: sage: q = QuantuminoSolver(0) sage: T = q.tiling_solver() sage: T Tiling solver of 16 pieces into the box (5, 8, 2) Rotation allowed: True Reflection allowed: False Reusing pieces allowed: False sage: rows = T.rows()                            # not tested (10 s) sage: len(rows)                                  # not tested (but fast) 5484 sage: x = T.dlx_solver()                         # long time (10 s) sage: x                                          # long time (fast) REFERENCES: - [1] Family Games America's Quantumino _ - [2] Quantumino - How to Play _ on Youtube - [3] Knuth, Donald (2000). "Dancing links". arXiv:cs/0011047 _. """ #***************************************************************************** #       Copyright (C) 2011 Sebastien Labbe # #  Distributed under the terms of the GNU General Public License (GPL) #  as published by the Free Software Foundation; either version 2 of #  the License, or (at your option) any later version. #                  http://www.gnu.org/licenses/ #***************************************************************************** from sage.structure.sage_object import SageObject from sage.plot.plot import Graphics from sage.plot.plot3d.platonic import cube from sage.plot.plot3d.shapes2 import text3d from sage.modules.free_module_element import vector from sage.combinat.tiling import Polyomino, TilingSolver ################################################ # Example:  The family games america: Quantumino ################################################ pentaminos = [] pentaminos.append(Polyomino([(0,0,0), (1,0,0), (1,1,0), (1,2,0), (1,1,1)], color='deeppink')) pentaminos.append(Polyomino([(0,0,0), (1,0,0), (1,1,0), (-1,0,0), (0,0,1)], color='deeppink')) pentaminos.append(Polyomino([(0,0,0), (1,0,0), (1,1,0), (1,2,0), (0,0,1)], color='green')) pentaminos.append(Polyomino([(0,0,0), (0,1,0), (0,2,0), (1,0,0), (1,0,1)], color='green')) pentaminos.append(Polyomino([(0,0,0), (1,0,0), (1,1,0), (1,0,1), (1,-1,1)], color='red')) pentaminos.append(Polyomino([(0,0,0), (1,0,0), (1,1,0), (1,0,1), (2,0,1)], color='red')) pentaminos.append(Polyomino([(0,0,0), (1,0,0), (1,1,0), (1,2,0), (1,2,1)], color='orange')) pentaminos.append(Polyomino([(0,0,0), (1,0,0), (0,1,0), (0,2,0), (0,2,1)], color='orange')) pentaminos.append(Polyomino([(0,0,0), (1,0,0), (0,1,0), (1,1,0), (0,0,1)], color='yellow')) pentaminos.append(Polyomino([(0,0,0), (1,0,0), (1,1,0), (1,1,1), (0,0,1)], color='yellow')) pentaminos.append(Polyomino([(0,0,0), (0,1,0), (1,1,0), (0,2,0), (1,1,1)], color='midnightblue')) pentaminos.append(Polyomino([(0,0,0), (1,0,0), (1,1,0), (1,0,1), (1,2,0)], color='darkblue')) pentaminos.append(Polyomino([(0,0,0), (1,0,0), (1,1,0), (1,1,1), (2,1,1)], color='blue')) pentaminos.append(Polyomino([(0,0,0), (0,1,0), (1,1,0), (1,1,1), (1,2,1)], color='blue')) pentaminos.append(Polyomino([(0,0,0), (1,0,0), (1,1,0), (2,1,0), (2,1,1)], color='purple')) pentaminos.append(Polyomino([(0,0,0), (0,1,0), (1,1,0), (1,2,0), (1,2,1)], color='purple')) pentaminos.append(Polyomino([(0,0,0), (1,0,0), (1,1,0), (1,0,1), (1,-1,0)], color='gray')) def show_pentaminos(box=(5,8,2)): r""" Show the 17 3-D pentaminos included in the game and the 5 \times 8 \times 2 box where 16 of them must fit. INPUT: - box - tuple of size three (optional, default: (5,8,2)), size of the box OUTPUT: 3D Graphic object EXAMPLES:: sage: from sage.games.quantumino import show_pentaminos sage: show_pentaminos()    # not tested (1s) To remove the frame do:: sage: show_pentaminos().show(frame=False)  # not tested (1s) """ G = Graphics() for i,p in enumerate(pentaminos): x = 3.5 * (i%4) y = 3.5 * (i/4) q = p + (x, y, 0) G += q.show3d() G += text3d(str(i), (x,y,2)) G += cube(color='gray',opacity=0.5).scale(box).translate((17,6,0)) # hack to set the aspect ratio to 1 a,b = G.bounding_box() a,b = map(vector, (a,b)) G.frame_aspect_ratio(tuple(b-a)) return G ############################## # Class QuantuminoState ############################## class QuantuminoState(SageObject): r""" A state of the Quantumino puzzle. Used to represent an solution or a partial solution of the Quantumino puzzle. INPUT: - pentos - list of 16 3d pentamino representing the (partial) solution - aside - 3d polyomino, the unused 3D pentamino EXAMPLES:: sage: from sage.games.quantumino import pentaminos, QuantuminoState sage: p = pentaminos[0] sage: q = pentaminos[5] sage: r = pentaminos[11] sage: S = QuantuminoState([p,q], r) sage: S Quantumino state where the following pentamino is put aside : Polyomino: [(0, 0, 0), (1, 0, 0), (1, 0, 1), (1, 1, 0), (1, 2, 0)], Color: darkblue :: sage: from sage.games.quantumino import QuantuminoSolver sage: QuantuminoSolver(3).solve().next()      # not tested (1.5s) Quantumino state where the following pentamino is put aside : Polyomino: [(0, 0, 0), (0, 1, 0), (0, 2, 0), (1, 0, 0), (1, 0, 1)], Color: green """ def __init__(self, pentos, aside): r""" EXAMPLES:: sage: from sage.games.quantumino import pentaminos, QuantuminoState sage: p = pentaminos[0] sage: q = pentaminos[5] sage: QuantuminoState([p], q) Quantumino state where the following pentamino is put aside : Polyomino: [(0, 0, 0), (1, 0, 0), (1, 0, 1), (1, 1, 0), (2, 0, 1)], Color: red """ assert all(isinstance(p, Polyomino) for p in pentos), "pentos must be an iterable of Polyomino" assert isinstance(aside, Polyomino), "aside must be a Polyomino" self._pentos = pentos self._aside = aside def __repr__(self): r""" EXAMPLES:: sage: from sage.games.quantumino import pentaminos, QuantuminoState sage: p = pentaminos[0] sage: q = pentaminos[5] sage: QuantuminoState([p], q) Quantumino state where the following pentamino is put aside : Polyomino: [(0, 0, 0), (1, 0, 0), (1, 0, 1), (1, 1, 0), (2, 0, 1)], Color: red """ return "Quantumino state where the following pentamino is put aside :\n%s" % self._aside def __iter__(self): r""" EXAMPLES:: sage: from sage.games.quantumino import pentaminos, QuantuminoState sage: p = pentaminos[0] sage: q = pentaminos[5] sage: r = pentaminos[11] sage: S = QuantuminoState([p,q], r) sage: for a in S: a Polyomino: [(0, 0, 0), (1, 0, 0), (1, 1, 0), (1, 1, 1), (1, 2, 0)], Color: deeppink Polyomino: [(0, 0, 0), (1, 0, 0), (1, 0, 1), (1, 1, 0), (2, 0, 1)], Color: red """ return iter(self._pentos) def list(self): r""" Return the list of 3d polyomino making the solution. EXAMPLES:: sage: from sage.games.quantumino import pentaminos, QuantuminoState sage: p = pentaminos[0] sage: q = pentaminos[5] sage: r = pentaminos[11] sage: S = QuantuminoState([p,q], r) sage: L = S.list() sage: L[0] Polyomino: [(0, 0, 0), (1, 0, 0), (1, 1, 0), (1, 1, 1), (1, 2, 0)], Color: deeppink sage: L[1] Polyomino: [(0, 0, 0), (1, 0, 0), (1, 0, 1), (1, 1, 0), (2, 0, 1)], Color: red """ return list(self) def show3d(self, size=0.85): r""" Return the solution as a 3D Graphic object. OUTPUT: 3D Graphic Object EXAMPLES:: sage: from sage.games.quantumino import QuantuminoSolver sage: s = QuantuminoSolver(0).solve().next()    # not tested (1.5s) sage: G = s.show3d()                            # not tested (<1s) sage: type(G)                                   # not tested To remove the frame:: sage: G.show(frame=False) # not tested To see the solution with Tachyon viewer:: sage: G.show(viewer='tachyon', frame=False) # not tested """ G = Graphics() for p in self: G += p.show3d(size=size) aside_pento = self._aside.canonical() + (2.5*size/0.75,-4*size/0.75,0) G += aside_pento.show3d(size=size) # hack to set the aspect ratio to 1 a,b = G.bounding_box() a,b = map(vector, (a,b)) G.frame_aspect_ratio(tuple(b-a)) return G ############################## # Class QuantuminoSolver ############################## class QuantuminoSolver(SageObject): r""" Return the Quantumino solver for the given box where one of the pentamino is put aside. INPUT: - aside - integer, from 0 to 16, the aside pentamino - box - tuple of size three (optional, default: (5,8,2)), size of the box EXAMPLES:: sage: from sage.games.quantumino import QuantuminoSolver sage: QuantuminoSolver(9) Quantumino solver for the box (5, 8, 2) Aside pentamino number: 9 sage: QuantuminoSolver(12, box=(5,4,4)) Quantumino solver for the box (5, 4, 4) Aside pentamino number: 12 """ def __init__(self, aside, box=(5,8,2)): r""" Constructor. EXAMPLES:: sage: from sage.games.quantumino import QuantuminoSolver sage: QuantuminoSolver(9) Quantumino solver for the box (5, 8, 2) Aside pentamino number: 9 """ if not  0 <= aside < 17: raise ValueError, "aside (=%s) must be between 0 and 16" % aside self._aside = aside self._box = box def __repr__(self): r""" String representation EXAMPLES:: sage: from sage.games.quantumino import QuantuminoSolver sage: QuantuminoSolver(0) Quantumino solver for the box (5, 8, 2) Aside pentamino number: 0 """ s = "Quantumino solver for the box %s\n" % (self._box, ) s += "Aside pentamino number: %s" % self._aside return s def tiling_solver(self): r""" Return the Tiling solver of the Quantumino Game where one of the pentamino is put aside. EXAMPLES:: sage: from sage.games.quantumino import QuantuminoSolver sage: QuantuminoSolver(0).tiling_solver() Tiling solver of 16 pieces into the box (5, 8, 2) Rotation allowed: True Reflection allowed: False Reusing pieces allowed: False sage: QuantuminoSolver(14).tiling_solver() Tiling solver of 16 pieces into the box (5, 8, 2) Rotation allowed: True Reflection allowed: False Reusing pieces allowed: False sage: QuantuminoSolver(14, box=(5,4,4)).tiling_solver() Tiling solver of 16 pieces into the box (5, 4, 4) Rotation allowed: True Reflection allowed: False Reusing pieces allowed: False """ pieces = pentaminos[:self._aside] + pentaminos[self._aside+1:] return TilingSolver(pieces, box=self._box) def solve(self, partial=None): r""" Return an iterator over the solutions where one of the pentamino is put aside. INPUT: - partial - string (optional, default: None), whether to include partial (incomplete) solutions. It can be one of the following: - None - include only complete solution - 'common' - common part between two consecutive solutions - 'incremental'` - one piece change at a time OUTPUT: iterator of QuantuminoState EXAMPLES: Get one solution:: sage: from sage.games.quantumino import QuantuminoSolver sage: s = QuantuminoSolver(8).solve().next()         # long time (9s) sage: s                                              # long time (fast) Quantumino state where the following pentamino is put aside : Polyomino: [(0, 0, 0), (0, 0, 1), (0, 1, 0), (1, 0, 0), (1, 1, 0)], Color: yellow sage: s.show3d()                                     # long time (< 1s) The explicit solution:: sage: for p in s: p                                  # long time (fast) Polyomino: [(0, 0, 0), (1, 0, 0), (1, 1, 0), (1, 1, 1), (1, 2, 0)], Color: deeppink Polyomino: [(0, 0, 1), (0, 1, 0), (0, 1, 1), (0, 2, 1), (1, 2, 1)], Color: deeppink Polyomino: [(0, 2, 0), (0, 3, 0), (0, 4, 0), (1, 4, 0), (1, 4, 1)], Color: green Polyomino: [(0, 3, 1), (1, 3, 1), (2, 2, 0), (2, 2, 1), (2, 3, 1)], Color: green Polyomino: [(1, 3, 0), (2, 3, 0), (2, 4, 0), (2, 4, 1), (3, 4, 0)], Color: red Polyomino: [(1, 0, 1), (2, 0, 0), (2, 0, 1), (2, 1, 0), (3, 0, 1)], Color: midnightblue Polyomino: [(0, 4, 1), (0, 5, 0), (0, 5, 1), (0, 6, 0), (1, 5, 0)], Color: red Polyomino: [(2, 1, 1), (3, 0, 0), (3, 1, 0), (3, 1, 1), (4, 0, 0)], Color: blue Polyomino: [(3, 2, 0), (4, 0, 1), (4, 1, 0), (4, 1, 1), (4, 2, 0)], Color: purple Polyomino: [(3, 2, 1), (3, 3, 0), (4, 2, 1), (4, 3, 0), (4, 3, 1)], Color: yellow Polyomino: [(3, 3, 1), (3, 4, 1), (4, 4, 0), (4, 4, 1), (4, 5, 0)], Color: blue Polyomino: [(0, 6, 1), (0, 7, 0), (0, 7, 1), (1, 5, 1), (1, 6, 1)], Color: purple Polyomino: [(1, 6, 0), (1, 7, 0), (1, 7, 1), (2, 7, 0), (3, 7, 0)], Color: darkblue Polyomino: [(2, 5, 0), (2, 6, 0), (3, 6, 0), (4, 6, 0), (4, 6, 1)], Color: orange Polyomino: [(2, 5, 1), (3, 5, 0), (3, 5, 1), (3, 6, 1), (4, 5, 1)], Color: gray Polyomino: [(2, 6, 1), (2, 7, 1), (3, 7, 1), (4, 7, 0), (4, 7, 1)], Color: orange Enumerate the solutions:: sage: it = QuantuminoSolver(0).solve() sage: it.next()                                          # not tested Quantumino state where the following pentamino is put aside : Polyomino: [(0, 0, 0), (1, 0, 0), (1, 1, 0), (1, 1, 1), (1, 2, 0)], Color: deeppink sage: it.next()                                          # not tested Quantumino state where the following pentamino is put aside : Polyomino: [(0, 0, 0), (1, 0, 0), (1, 1, 0), (1, 1, 1), (1, 2, 0)], Color: deeppink With the partial solutions included, one can see the evolution between consecutive solutions (an animation would be better):: sage: it = QuantuminoSolver(0).solve(partial='common') sage: it.next().show3d()               # not tested (2s) sage: it.next().show3d()               # not tested (< 1s) sage: it.next().show3d()               # not tested (< 1s) Generalizations of the game inside different boxes:: sage: QuantuminoSolver(7, (4,4,5)).solve().next()       # long time (2s) Quantumino state where the following pentamino is put aside : Polyomino: [(0, 0, 0), (0, 1, 0), (0, 2, 0), (0, 2, 1), (1, 0, 0)], Color: orange sage: QuantuminoSolver(7, (2,2,20)).solve().next()      # long time (1s) Quantumino state where the following pentamino is put aside : Polyomino: [(0, 0, 0), (0, 1, 0), (0, 2, 0), (0, 2, 1), (1, 0, 0)], Color: orange sage: QuantuminoSolver(3, (2,2,20)).solve().next()      # long time (1s) Quantumino state where the following pentamino is put aside : Polyomino: [(0, 0, 0), (0, 1, 0), (0, 2, 0), (1, 0, 0), (1, 0, 1)], Color: green If the volume of the box is not 80, there is no solution:: sage: QuantuminoSolver(7, box=(3,3,9)).solve().next() Traceback (most recent call last): ... StopIteration If the box is too small, there is no solution:: sage: QuantuminoSolver(4, box=(40,2,1)).solve().next() Traceback (most recent call last): ... StopIteration """ T = self.tiling_solver() aside = pentaminos[self._aside] for pentos in T.solve(partial=partial): yield QuantuminoState(pentos, aside) def number_of_solutions(self): r""" Return the number of solutions. OUTPUT: integer EXAMPLES:: sage: from sage.games.quantumino import QuantuminoSolver sage: QuantuminoSolver(4, box=(3,2,2)).number_of_solutions() 0 This computation takes several days:: sage: QuantuminoSolver(0).number_of_solutions()                # not tested ??? hundreds of millions ??? """ return self.tiling_solver().number_of_solutions()