I came across a 2014 paper by Natalie Frank and two guys from Wollongong, Fractal Dual Substitution Tilings. A lot of it went wayy over my head, but as so often happens, a picture told enough to write the program:
So here they’ve made up 2 arbitrary paths for each colour tile of a Thue-Morse square tiling. Or rather, the dual of a tiling – the shapes you get when you join the centers of the existing tiles.
++++For example, the first little blue square in the upper picture has a ‘top’ and ‘right’ radius drawn – these are substituted by the larger top and right paths in the picture. The white little squares are substituted by the larger paths in the picture, etc, repeating until the changes are too small to see at the given scale, producing the Fractal pic on the right.
Larger version of their original T-M tiles:
++++I got that working, tried a few tilings, before finding it too limited, the shapes repeating too often, at least in a square arrangement, so I started one with 3 coloured tiles. And I used whatever arrangement of source fractals I wanted (i.e. not the strict Thue-Morse arrangement as here), and a random arrangement of the tiles in the final tiling. Also it uses flips on the NW-SE diagonal (I kept the same points as in the paper where the lines hit the edge of the square) making 6 different tiles randomly arranged. There’s an option in the program to print the square grid over the tiling or not.
Cython program. It saves the fractal maps and tiling as 2 png images.
Run it at the Sage command line with %attach FDS3types.pyx then OK(), or in a Sage notebook with %cython as the first line of the program.
(It should work with more than 3 tiles with minimal alteration, just widening the ARRAYWID etc so the first pic is wide enough to contain 4+ cells, add 1 to COLOURS and another colour to shades[] array, etc)
#FDS3types.pyx - #modified from Thue-Morse 2D tiling fractal program - Apr 2017 #from N Frank paper "Fractal Dual Substitution Tilings" p3 import numpy as np cimport numpy as np from scipy import misc from libc.stdlib cimport rand DEF MAXPTS=12000 #NB this must be larger than number of points in each tile DEF MAXLEVEL=4 DEF ARRAYWID=850 DEF ARRAYHEI=400 DEF MARG=20 DEF COLOURS=3 #White, Blue, Green DEF DIRECTIONS=4 #Top, Right, Bottom, Left DEF MAXPATHSEGS=13 #NB this must be > largest value in numrules[][] array DEF INITSIDE=4 DEF SIDE=INITSIDE**MAXLEVEL DEF TILEWID=8 # times 256 =2048 DEF PICWID=TILEWID*SIDE DEF DRAWTILINGGRID=1 #set to 0 =DONT DRAW GRID on tiling pic, 1 = DRAW cdef: enum: T=0, R=1, B=2, L=3 #type Wh=0,Bl=1,Gr=2 #subcol struct point: int x,y point C[COLOURS][MAXPTS] #C[0]=white pixels, C[1]=blue int col, total[COLOURS] #total num of white, blue pixels stored so far int numrules[COLOURS][DIRECTIONS] #stores num of steps in each subpath struct pa: int subcol,type point loc pa path[COLOURS][DIRECTIONS][MAXPATHSEGS], it unsigned short Black[3], shade[3][3], pic[PICWID][PICWID][3], fillcol[3] Fill(int x, int y): #colour in tile with fillcol if pic[x][y][0]==255: #if red=255, i.e. if White pic[x][y][:]=fillcol if x<PICWID-1: Fill(x+1,y) if y<PICWID-1: #do upper right Fill(x+1,y+1) if y>0: #lower right Fill(x+1,y-1) if x>0: Fill(x-1,y) if y<PICWID-1: #do upper left Fill(x-1,y+1) if y>0: #lower left Fill(x-1,y-1) if y<PICWID-1: Fill(x,y+1) if y>0: Fill(x,y-1) int Done(point d,int level): if level==MAXLEVEL: C[col][total[col]]=d #add point to end of list of pts total[col]+=1 return 1 # i.e. otherwise return 0 Black[:]=[0,0,0] Gray=[100,100,100] LightGray=[180,180,180] shade[Wh][:]=[255,255,255] #white shade[Bl][:]=[80,80,255] #blue shade[Gr][:]=[0,180,0] #green # ---------------- change the below settings to change fractal. # the number path[][][num] must = the number of segments in list. # and those 4 nums for each colour must be put in its numrules[][] list. # make sure MAXPATHSEGS>greatest num. and MAXPTS>points in each fract segment numrules[Wh][:]=[1,11,11,5] numrules[Bl][:]=[5,11,11,1] numrules[Gr][:]=[7,1,9,11] path[Wh][T][:1]=[[Bl,T,[1,0]]] # i.e. the top path of the white tile has 1 segment: # a blue top path in cell [1,0] (2nd column in top row - they're numbered 0-3) path[Wh][R][:11]=[[Bl,R,[1,0]],[Gr,L,[2,0]],[Gr,B,[2,0]],[Bl,T,[2,1]], [Bl,B,[2,1]],[Gr,T,[2,2]],[Gr,R,[2,2]], [Wh,L,[3,2]],[Wh,T,[3,2]],[Gr,B,[3,1]],[Gr,R,[3,1]]] path[Wh][B][:11]=[[Bl,B,[1,0]],[Wh,T,[1,1]],[Wh,B,[1,1]],[Bl,T,[1,2]], [Bl,L,[1,2]],[Gr,R,[0,2]],[Gr,B,[0,2]],[Wh,T,[0,3]],[Wh,R,[0,3]], [Bl,L,[1,3]],[Bl,B,[1,3]]] path[Wh][L][:5]=[[Bl,L,[1,0]],[Gr,R,[0,0]],[Gr,B,[0,0]],[Bl,T,[0,1]],[Bl,L,[0,1]]] path[Bl][T][:5]=[[Gr,T,[0,1]],[Gr,B,[0,0]],[Gr,R,[0,0]],[Wh,L,[1,0]],[Wh,T,[1,0]]] path[Bl][R][:11]=[[Gr,R,[0,1]],[Bl,L,[1,1]],[Bl,R,[1,1]],[Gr,L,[2,1]], [Gr,T,[2,1]],[Wh,B,[2,0]],[Wh,R,[2,0]],[Gr,L,[3,0]],[Gr,B,[3,0]],[Wh,T,[3,1]], [Wh,R,[3,1]]] path[Bl][B][:11]=[[Gr,B,[0,1]],[Bl,T,[0,2]],[Bl,R,[0,2]],[Gr,L,[1,2]], [Gr,R,[1,2]],[Bl,L,[2,2]],[Bl,B,[2,2]],[Bl,T,[2,3]],[Bl,L,[2,3]],[Wh,R,[1,3]], [Wh,B,[1,3]]] path[Bl][L][:1]=[[Gr,L,[0,1]]] path[Gr][T][:7]=[[Wh,T,[3,1]],[Gr,B,[3,0]],[Gr,L,[3,0]],[Bl,R,[2,0]], [Bl,L,[2,0]],[Wh,R,[1,0]],[Wh,T,[1,0]]] path[Gr][R][:1]=[[Wh,R,[3,1]]] path[Gr][B][:9]=[[Bl,B,[1,3]],[Bl,R,[1,3]],[Wh,L,[2,3]],[Wh,R,[2,3]],[Bl,L,[3,3]], [Bl,T,[3,3]],[Wh,B,[3,2]],[Wh,T,[3,2]],[Wh,B,[3,1]]] path[Gr][L][:11]=[[Wh,L,[0,1]],[Wh,R,[0,1]],[Bl,L,[1,1]],[Bl,B,[1,1]], [Wh,T,[1,2]],[Wh,R,[1,2]],[Wh,L,[2,2]],[Wh,T,[2,2]],[Wh,B,[2,1]],[Wh,R,[2,1]], [Wh,L,[3,1]]] cdef Do(pa which,point where, int level): cdef int n where=[where.x*INITSIDE+which.loc.x,where.y*INITSIDE+which.loc.y] if not Done(where,level): for n in xrange(numrules[which.subcol][which.type]): Do(path[which.subcol][which.type][n],where,level+1) cpdef OK(): global fillcol,col cdef: int i, j, k, c, xx, yy, m, n, f, xloc np.ndarray[np.uint8_t,ndim=3] a= \ np.empty((ARRAYHEI,ARRAYWID,3),dtype=np.uint8) np.ndarray[np.uint8_t,ndim=3] t= \ np.empty((PICWID,PICWID,3),dtype=np.uint8) point z=[0,0] total[:]=[0,0,0] for col in xrange(COLOURS): #White, Blue, Green for j in xrange(DIRECTIONS): #T,R,B,L Do([col,j,z],z,0) #construct the fractal paths a.fill(255) #make array for tiles image white for j in xrange(PICWID): for i in xrange(PICWID): pic[i][j][0]=255 #make tiling image white #well- red is white enough - only red tested later by Fill for i in xrange(SIDE): #coloured squares for k in xrange(SIDE): for j in xrange(3): a[MARG+k,MARG*2+SIDE+i,j]=shade[Bl][j] a[MARG+k,MARG*3+2*SIDE+i,j]=shade[Gr][j] DEF UNIT=MARG+SIDE #cell+gap; width of whole repeating unit DEF CELLWID=16 DEF HALF=CELLWID/2 DEF GAP=20 #between tiles and cells DEF CELLTOP=UNIT+GAP DEF XMARG=MARG+100 for k in xrange(3): for i in xrange(COLOURS): xloc=MARG+UNIT*i for j in xrange(total[i]): #plot fractal points a[MARG+C[i][j].y,C[i][j].x+xloc,k]=0 #black for j in xrange(SIDE): #black frame around cells a[MARG+j,xloc,k]=0 #LHS a[MARG+j,(i+1)*UNIT,k]=0 #RHS a[MARG,xloc+j,k]=0 #top a[MARG+SIDE,xloc+j,k]=0 #bot #first, grey over whole cell - so that white can be seen for type 1. for i in xrange(COLOURS): xloc=XMARG+UNIT*i for j in xrange(CELLWID*4): for k in xrange(CELLWID*4): a[CELLTOP+k,xloc+j,:]=LightGray for j in xrange(DIRECTIONS): #do cell colours for k in xrange(numrules[i][j]): it=path[i][j][k] xx=xloc+it.loc.x*CELLWID yy=CELLTOP+it.loc.y*CELLWID for m in xrange(CELLWID): for n in xrange(CELLWID): for c in xrange(3): a[yy+n,xx+m,c]=shade[it.subcol][c] for j in xrange(5): #gray border lines for k in xrange(4*CELLWID): a[CELLTOP+k,xloc+j*CELLWID,:]=Gray a[CELLTOP+j*CELLWID,xloc+k,:]=Gray for j in xrange(DIRECTIONS): #black path lines for k in xrange(numrules[i][j]): it=path[i][j][k] xx=xloc+HALF+it.loc.x*CELLWID yy=CELLTOP+HALF+it.loc.y*CELLWID if it.type==T: for n in xrange(HALF+1): a[yy-HALF+n,xx,:]=[0,0,0] #black elif it.type==R: for n in xrange(HALF+1): a[yy,xx+n,:]=[0,0,0] elif it.type==B: for n in xrange(HALF+1): a[yy+n,xx,:]=[0,0,0] elif it.type==L: for n in xrange(HALF+1): a[yy,xx-HALF+n,:]=[0,0,0] misc.imsave('FDS3types-fract.png',a) print total[0],total[1],total[2],SIDE #save sample pic..4x4 random cells for j in xrange(TILEWID): for k in xrange(TILEWID): if DRAWTILINGGRID>0: for i in xrange(j*SIDE,(j+1)*SIDE,2): #dotted line pic[i][k*SIDE][:]=Black pic[k*SIDE][i][:]=Black m=rand()%COLOURS f=rand()%2 #to flip or not for n in xrange(total[m]): if f==0: pic[j*SIDE+C[m][n].x][k*SIDE+C[m][n].y][:]=Black else: #flip - swap x and y pic[j*SIDE+C[m][n].y][k*SIDE+C[m][n].x][:]=Black #fill cells with rand col for j in xrange(0,PICWID,10): #test each 10x10 square for unfilledness for k in xrange(0,PICWID,10): if pic[j][k][0]==255: #found a white spot - fill fillcol[:]=[rand()%80,rand()%120+100,rand()%80] print j,k,fillcol[0],fillcol[1],fillcol[2] Fill(j,k) for c in xrange(3): for j in xrange(PICWID): for k in xrange(PICWID): t[j,k,c]=pic[k][j][c] #swaps x and y.. misc.imsave('FDS3types-tiling.png',t)
(Apr 2017)
[Update Sept 2017: Arghh, obviously that program was written before I read Refactoring & Clean Code. What a mess. Sorry.]