shrx / spectre

Python script for generating tilings of the weakly chiral aperiodic monotile Tile(1,1) "Spectre".
13 stars 2 forks source link

Some matplotlib/numpy version of the spectre script #1

Open logari81 opened 1 year ago

logari81 commented 1 year ago

Thanks for this nice port to python, here there is a version of your code ported to numpy and using matplotlib for plotting

#!/usr/bin/python3
import numpy as np
from time import time
import matplotlib.pyplot as plt

PI = np.pi

# increase this number for larger tilings.
N_ITERATIONS = 4

IDENTITY = np.array([[1,0,0],
                     [0,1,0]], 'float32')
TILE_NAMES = ["Gamma", "Delta", "Theta", "Lambda", "Xi", "Pi", "Sigma", "Phi", "Psi"]

COLOR_MAP = {"Gamma":  np.array((211,  95,  95),'f')/255.,
             "Gamma1": np.array((200,  55,  55),'f')/255.,
             "Gamma2": np.array((222, 135, 135),'f')/255.,
             "Delta":  np.array((  0, 255, 255),'f')/255.,
             "Theta":  np.array((255, 170, 238),'f')/255.,
             "Lambda": np.array((128, 128, 128),'f')/255.,
             "Xi":     np.array((145,  95, 211),'f')/255.,
             "Pi":     np.array(( 95,  95, 211),'f')/255.,
             "Sigma":  np.array(( 85, 212,   0),'f')/255.,
             "Phi":    np.array((255, 179, 128),'f')/255.,
             "Psi":    np.array((255, 221,  85),'f')/255.}

SPECTRE_POINTS = np.array([(0.0,              0.0),
                           (1.0,              0.0),
                           (1.5,              -np.sqrt(3)/2),
                           (1.5+np.sqrt(3)/2, 0.5-np.sqrt(3)/2),
                           (1.5+np.sqrt(3)/2, 1.5-np.sqrt(3)/2),
                           (2.5+np.sqrt(3)/2, 1.5-np.sqrt(3)/2),
                           (3+np.sqrt(3)/2,   1.5),
                           (3.0,              2.0),
                           (3-np.sqrt(3)/2,   1.5),
                           (2.5-np.sqrt(3)/2, 1.5+np.sqrt(3)/2),
                           (1.5-np.sqrt(3)/2, 1.5+np.sqrt(3)/2),
                           (0.5-np.sqrt(3)/2, 1.5+np.sqrt(3)/2),
                           (-np.sqrt(3)/2,    1.5),
                           (0.0,              1.0)], 'float32')
SPECTRE_QUAD = SPECTRE_POINTS[[3,5,7,11],:]

def mul(A, B):
    AB = A.copy()
    AB[:,:2] = A[:,:2].dot(B[:,:2]) 
    AB[:,2] += A[:,:2].dot(B[:,2])
    return AB

class Tile:
    def __init__(self, label):
        self.label = label
        self.quad = SPECTRE_QUAD.copy()

    def draw(self, polygons, tile_transformation=IDENTITY.copy()):
        vertices = SPECTRE_POINTS.dot(tile_transformation[:,:2].T) + tile_transformation[:,2]
        polygons.append((vertices, COLOR_MAP[self.label]))

class MetaTile:
    def __init__(self, tiles=[], transformations=[], quad=SPECTRE_QUAD.copy()):
        self.tiles = tiles
        self.transformations = transformations
        self.quad = quad

    def draw(self, polygons, transformation=IDENTITY.copy()):
        for tile, trsf in zip(self.tiles, self.transformations):
           tile.draw(polygons, mul(transformation, trsf))

def buildSpectreBase():
    ttrans = np.array([[1,0,SPECTRE_POINTS[8,0]],
                       [0,1,SPECTRE_POINTS[8,1]]])
    trot = np.array([[np.cos(PI/6),-np.sin(PI/6),0.],
                     [np.sin(PI/6), np.cos(PI/6),0.]],'float32')
    trsf = mul(ttrans, trot)
    tiles = {}
    tiles["Gamma"] = MetaTile(tiles=[Tile("Gamma1"),Tile("Gamma2")],
                                     transformations=[IDENTITY.copy(),trsf],
                                     quad=SPECTRE_QUAD.copy())
    for label in TILE_NAMES:
        if label != "Gamma":
            tiles[label] = Tile(label) 
    return tiles

def buildSupertiles(input_tiles):
    # First, use any of the nine-unit tiles in "tiles" to obtain a
    # list of transformation matrices for placing tiles within supertiles.
    quad = input_tiles["Delta"].quad

    transformations = [IDENTITY.copy()]
    total_angle = 0
    trot = IDENTITY.copy()
    transformed_quad = quad
    for _angle, _from, _to in ((   PI/3, 3, 1),
                               (     0., 2, 0),
                               (   PI/3, 3, 1),
                               (   PI/3, 3, 1),
                               (     0., 2, 0),
                               (   PI/3, 3, 1),
                               (-2*PI/3, 3, 3)):
        if _angle != 0:
            total_angle += _angle
            trot = np.array([[1, 0,0],[0,1,0]])*np.cos(total_angle) \
                  +np.array([[0,-1,0],[1,0,0]])*np.sin(total_angle)
            transformed_quad = quad.dot(trot[:,:2].T) # + trot[:,2]
        last_trsf = transformations[-1]
        ttrans = IDENTITY.copy()
        ttrans[:,2] = last_trsf[:,:2].dot(quad[_from,:]) + last_trsf[:,2] \
                     -transformed_quad[_to,:]
        transformations.append(mul(ttrans, trot))

    R = np.array([[-1,0,0],[ 0,1,0]], 'float32')
    transformations = [ mul(R, trsf) for trsf in transformations ]

    # Now build the actual supertiles, labeling appropriately.
    super_quad = quad[[2,1,2,1],:]
    for i,itrsf in enumerate([6,5,3,0]):
        trsf = transformations[itrsf]
        super_quad[i,:] = trsf[:,:2].dot(super_quad[i,:]) + trsf[:,2]

    tiles = {}
    for label, substitutions in (("Gamma",  ("Pi",  "Delta", None,  "Theta", "Sigma", "Xi",  "Phi",    "Gamma")),
                                 ("Delta",  ("Xi",  "Delta", "Xi",  "Phi",   "Sigma", "Pi",  "Phi",    "Gamma")),
                                 ("Theta",  ("Psi", "Delta", "Pi",  "Phi",   "Sigma", "Pi",  "Phi",    "Gamma")),
                                 ("Lambda", ("Psi", "Delta", "Xi",  "Phi",   "Sigma", "Pi",  "Phi",    "Gamma")),
                                 ("Xi",     ("Psi", "Delta", "Pi",  "Phi",   "Sigma", "Psi", "Phi",    "Gamma")),
                                 ("Pi",     ("Psi", "Delta", "Xi",  "Phi",   "Sigma", "Psi", "Phi",    "Gamma")),
                                 ("Sigma",  ("Xi",  "Delta", "Xi",  "Phi",   "Sigma", "Pi",  "Lambda", "Gamma")),
                                 ("Phi",    ("Psi", "Delta", "Psi", "Phi",   "Sigma", "Pi",  "Phi",    "Gamma")),
                                 ("Psi",    ("Psi", "Delta", "Psi", "Phi",   "Sigma", "Psi", "Phi",    "Gamma"))):
        tiles[label] =\
            MetaTile(tiles=[input_tiles[subst] for subst in substitutions if subst],
                     transformations=[trsf for subst, trsf in zip(substitutions, transformations) if subst],
                     quad=super_quad)
    return tiles

start = time()
tiles = buildSpectreBase()
for _ in range(N_ITERATIONS):
    tiles = buildSupertiles(tiles)
time1 = time()-start
print(f"supertiling loop took {round(time1, 4)} seconds")

start = time()
polygons = []
tiles["Delta"].draw(polygons)
time2 = time()-start
print(f"tile recursion loop took {round(time2, 4)} seconds, generated {len(polygons)} tiles")

plt.figure(figsize=(8, 8))
plt.axis('equal')
for pts,color in polygons:
    plt.fill(pts[:,0],pts[:,1],facecolor=color)
plt.show()
logari81 commented 1 year ago

and here a dictionary-free version of the same code

#!/usr/bin/python3
import numpy as np
from time import time
import matplotlib.pyplot as plt

PI = np.pi

# increase this number for larger tilings.
N_ITERATIONS = 0

IDENTITY = np.array([[1,0,0],
                     [0,1,0]], 'float32')

COLOR_MAP = (np.array((211,  95,  95),'f')/255., # 0: Gamma
             np.array((  0, 255, 255),'f')/255., # 1: Delta
             np.array((255, 170, 238),'f')/255., # 2: Theta
             np.array((128, 128, 128),'f')/255., # 3: Lambda
             np.array((145,  95, 211),'f')/255., # 4: Xi
             np.array(( 95,  95, 211),'f')/255., # 5: Pi
             np.array(( 85, 212,   0),'f')/255., # 6: Sigma
             np.array((255, 179, 128),'f')/255., # 7: Phi
             np.array((255, 221,  85),'f')/255., # 8: Psi
             np.array((200,  55,  55),'f')/255., # 9: Gamma1
             np.array((222, 135, 135),'f')/255.) #10: Gamma2

SPECTRE_POINTS = np.array([(0.0,              0.0),
                           (1.0,              0.0),
                           (1.5,              -np.sqrt(3)/2),
                           (1.5+np.sqrt(3)/2, 0.5-np.sqrt(3)/2),
                           (1.5+np.sqrt(3)/2, 1.5-np.sqrt(3)/2),
                           (2.5+np.sqrt(3)/2, 1.5-np.sqrt(3)/2),
                           (3+np.sqrt(3)/2,   1.5),
                           (3.0,              2.0),
                           (3-np.sqrt(3)/2,   1.5),
                           (2.5-np.sqrt(3)/2, 1.5+np.sqrt(3)/2),
                           (1.5-np.sqrt(3)/2, 1.5+np.sqrt(3)/2),
                           (0.5-np.sqrt(3)/2, 1.5+np.sqrt(3)/2),
                           (-np.sqrt(3)/2,    1.5),
                           (0.0,              1.0)], 'float32')
SPECTRE_QUAD = SPECTRE_POINTS[[3,5,7,11],:]

def mul(A, B):
    AB = A.copy()
    AB[:,:2] = A[:,:2].dot(B[:,:2]) 
    AB[:,2] += A[:,:2].dot(B[:,2])
    return AB

class Tile:
    def __init__(self, tile_type):
        self.type = tile_type
        self.quad = SPECTRE_QUAD.copy()

    def add_to_cluster(self, cluster, tile_transformation=IDENTITY.copy()):
        cluster.append((self.type, tile_transformation))
    def draw(self, polygons, tile_transformation=IDENTITY.copy()):
        vertices = SPECTRE_POINTS.dot(tile_transformation[:,:2].T) + tile_transformation[:,2]
        polygons.append((vertices, COLOR_MAP[self.type]))

class MetaTile:
    def __init__(self, tiles=[], transformations=[], quad=SPECTRE_QUAD.copy()):
        self.tiles = tiles
        self.transformations = transformations
        self.quad = quad

    def add_to_cluster(self, cluster, transformation=IDENTITY.copy()):
        for tile, trsf in zip(self.tiles, self.transformations):
           tile.add_to_cluster(cluster, mul(transformation, trsf))
    def draw(self, polygons, transformation=IDENTITY.copy()):
        for tile, trsf in zip(self.tiles, self.transformations):
           tile.draw(polygons, mul(transformation, trsf))

def buildSpectreBase():
    ttrans = np.array([[1,0,SPECTRE_POINTS[8,0]],
                       [0,1,SPECTRE_POINTS[8,1]]])
    trot = np.array([[np.cos(PI/6),-np.sin(PI/6),0.],
                     [np.sin(PI/6), np.cos(PI/6),0.]],'float32')
    trsf = mul(ttrans, trot)
    tiles = [MetaTile(tiles=[Tile(9),Tile(10)],
                      transformations=[IDENTITY.copy(),trsf],
                      quad=SPECTRE_QUAD.copy())]
    tiles += [Tile(i) for i in range(1,9)]
    return tiles

def buildSupertiles(input_tiles):
    # First, use any of the nine-unit tiles in "tiles" to obtain a
    # list of transformation matrices for placing tiles within supertiles.
    quad = input_tiles[1].quad

    transformations = [IDENTITY.copy()]
    total_angle = 0
    trot = IDENTITY.copy()
    transformed_quad = quad
    for _angle, _from, _to in ((   PI/3, 3, 1),
                               (     0., 2, 0),
                               (   PI/3, 3, 1),
                               (   PI/3, 3, 1),
                               (     0., 2, 0),
                               (   PI/3, 3, 1),
                               (-2*PI/3, 3, 3)):
        if _angle != 0:
            total_angle += _angle
            trot = np.array([[1, 0,0],[0,1,0]])*np.cos(total_angle) \
                  +np.array([[0,-1,0],[1,0,0]])*np.sin(total_angle)
            transformed_quad = quad.dot(trot[:,:2].T) # + trot[:,2]
        last_trsf = transformations[-1]
        ttrans = IDENTITY.copy()
        ttrans[:,2] = last_trsf[:,:2].dot(quad[_from,:]) + last_trsf[:,2] \
                     -transformed_quad[_to,:]
        transformations.append(mul(ttrans, trot))

    R = np.array([[-1,0,0],[ 0,1,0]], 'float32')
    transformations = [ mul(R, trsf) for trsf in transformations ]

    # Now build the actual supertiles, labeling appropriately.
    super_quad = quad[[2,1,2,1],:]
    for i,itrsf in enumerate([6,5,3,0]):
        trsf = transformations[itrsf]
        super_quad[i,:] = trsf[:,:2].dot(super_quad[i,:]) + trsf[:,2]

    tiles = []
    for substitutions in ((5, 1, -1, 2, 6, 4, 7, 0),
                          (4, 1,  4, 7, 6, 5, 7, 0),
                          (8, 1,  5, 7, 6, 5, 7, 0),
                          (8, 1,  4, 7, 6, 5, 7, 0),
                          (8, 1,  5, 7, 6, 8, 7, 0),
                          (8, 1,  4, 7, 6, 8, 7, 0),
                          (4, 1,  4, 7, 6, 5, 3, 0),
                          (8, 1,  8, 7, 6, 5, 7, 0),
                          (8, 1,  8, 7, 6, 8, 7, 0)):
        tiles.append(MetaTile(tiles=[input_tiles[subst] for subst in substitutions if subst >= 0],
                              transformations=[trsf for subst, trsf in zip(substitutions, transformations) if subst >= 0],
                              quad=super_quad))
    return tiles

start = time()
tiles = buildSpectreBase()
for _ in range(N_ITERATIONS):
    tiles = buildSupertiles(tiles)
time1 = time()-start
print(f"supertiling loop took {round(time1, 4)} seconds")

start = time()
polygons = []
tiles[1].draw(polygons)
time2 = time()-start
print(f"tile recursion loop took {round(time2, 4)} seconds, generated {len(polygons)} tiles")

plt.figure(figsize=(8, 8))
plt.axis('equal')
for pts,color in polygons:
    plt.fill(pts[:,0],pts[:,1],facecolor=color)
plt.show()
shrx commented 1 year ago

Hello, thanks for your contribution! Feel free to open a pull request.

reversi-fun commented 9 months ago

Hi. It's probably a good thing that we stopped keeping all the vertex coordinates of the spectre shape, as it allows us to draw larger shapes.

@logari81 Your version, which held the information for transform as a label in string format, was several times faster than the original version. but dictionary-free version is not good.

I also make Python code for more faster and more greater spectre tiles. and It made any ration of Spectre tile(a,b)

Prease see my Pullrequest.

The comparison results are shown below. (drowsvg is faster 30Times, file size shoter 5Times).