Hexagonal City Growth Model

Yingjun Mou
Data Mining the City
5 min readSep 9, 2018

STUDENT NAME: YINGJUN (RENZO) MOU — — MSAAD
DATE: 09/09/2018

DESCRIPTION:
This is a script I created to explore the mathematic rules of the growth of a hexagonal city.
The hexagonal city should have the advantages of both orthogonal grid city and the radial ones, since it’s the synthesis of the two.
In this model, there are few algorithms created
# (1) the hexagonal grid will “grow” by randomly choosing a starting point and expands on the 6 different directions.
# (2) If a triangular piece of land was enclosed by the grid, then it will be occupied and buildings will be built
# (3) If their are 6 adjacent occupied blocks to form a occupied hexagon, there will be a park created in the centroid of that hexagonal neighborhood.
# (4) The conditions of how the triangular block was occupied are constantly changing in a dynamic way. Sometimes, it’s one large monolithic block (such as a parking lot), sometimes it’s subdivided into small buildings.

The following is the GIF I created to simulate the growing process:

The screenshot of a developed hexagonal city:

The original sketch:

The source code I wrote:

import random
from random import randint
class Pt(object):
# x, y, degree
def __init__(self, x, y):
self.x = x
self.y = y
self.degree = 0

# overrite the equal function of class Pt
def __eq__(self, other):
if isinstance(other, Pt):
return ( abs(self.x-other.x)<tolerance and abs(self.y-other.y)<tolerance )
return False

def __cmp__(self, other):
if(abs(self.x-other.x)<tolerance):
return cmp(self.y, other.y)
return cmp(self.x, other.x)

def distance(self, other):
dx = abs(self.x - other.x)
dy = abs(self.y - other.y)
return sqrt(dx**2 + dy**2)

class Ln(object):
# pt1, pt2
def __init__(self, pt1, pt2):
self.pt1 = pt1
self.pt2 = pt2

# overrite the equal function of class Ln
def __eq__(self, other):
if isinstance(other, Ln):
return (self.pt1==other.pt1 and self.pt2==other.pt2) or (self.pt2==other.pt1 and self.pt1==other.pt2)
return False

class TriBlock(object):
# ln1, ln2, ln3, vlist (sorted by x value in ascending order)
def __init__(self, ln1, ln2, ln3):
self.ln1 = ln1
self.ln2 = ln2
self.ln3 = ln3
self.vlist = []

va = ln1.pt1
if (ln2.pt1 != va):
vb = ln2.pt1
else:
vb = ln2.pt2
if (ln3.pt1 != va and ln3.pt1 != vb):
vc = ln3.pt1
else:
vc = ln3.pt2

self.vlist.append(va)
self.vlist.append(vb)
self.vlist.append(vc)
self.vlist.sort()

def __eq__(self, other):
check1 = False
check2 = False
check3 = False
if isinstance(other, TriBlock):
if (self.ln1==other.ln1 and self.ln2==other.ln2 and self.ln3==other.ln3):
check1 = True
if (self.ln1==other.ln2 and self.ln2==other.ln3 and self.ln3==other.ln1):
check2 = True
if (self.ln1==other.ln3 and self.ln2==other.ln1 and self.ln3==other.ln2):
check3 = True
return (check1 or check2 or check3)

def offset (self, d):
# CASE 0: exception
if (self.vlist[0].y != self.vlist[2].y):
print("Error")
return
# CASE 1: the triangle points upward
elif (self.vlist[1].y < self.vlist[0].y):
v1 = Pt (self.vlist[0].x + sqrt(3) * 0.5 *d, self.vlist[0].y - 0.5 * d)
v2 = Pt (self.vlist[1].x, self.vlist[1].y + d)
v3 = Pt (self.vlist[2].x - sqrt(3) * 0.5 *d, self.vlist[2].y - 0.5 * d)
# CASE 2: the triangle points downward
else:
v1 = Pt (self.vlist[0].x + sqrt(3) * 0.5 *d, self.vlist[0].y + 0.5 * d)
v2 = Pt (self.vlist[1].x, self.vlist[1].y - d)
v3 = Pt (self.vlist[2].x - sqrt(3) * 0.5 *d, self.vlist[2].y + 0.5 * d)
return TriBlock (Ln(v1, v2), Ln(v2, v3), Ln(v3, v1))

def subdivide(self, level):
if (level==0):
return [self]
elif (level==1):
v0 = self.vlist[0]
v1 = self.vlist[1]
v2 = self.vlist[2]
v3 = Pt((v0.x+v1.x)/2, (v0.y+v1.y)/2)
v4 = Pt((v0.x+v2.x)/2, (v0.y+v2.y)/2)
v5 = Pt((v2.x+v1.x)/2, (v2.y+v1.y)/2)
tri1 = TriBlock(Ln(v0,v3), Ln(v3,v4), Ln(v4,v0))
tri2 = TriBlock(Ln(v1,v3), Ln(v3,v5), Ln(v5,v1))
tri3 = TriBlock(Ln(v3,v4), Ln(v4,v5), Ln(v5,v3))
tri4 = TriBlock(Ln(v2,v4), Ln(v4,v5), Ln(v5,v2))
returnlst = [tri1, tri2, tri3, tri4]
return returnlst
elif (level ==2):
v0 = self.vlist[0]
v1 = self.vlist[1]
v2 = self.vlist[2]
v3 = Pt((v0.x+v1.x)/2, (v0.y+v1.y)/2)
v4 = Pt((v0.x+v2.x)/2, (v0.y+v2.y)/2)
v5 = Pt((v2.x+v1.x)/2, (v2.y+v1.y)/2)
tri1 = TriBlock(Ln(v0,v3), Ln(v3,v4), Ln(v4,v0))
tri2 = TriBlock(Ln(v1,v3), Ln(v3,v5), Ln(v5,v1))
tri3 = TriBlock(Ln(v3,v4), Ln(v4,v5), Ln(v5,v3))
tri4 = TriBlock(Ln(v2,v4), Ln(v4,v5), Ln(v5,v2))
returnlst = tri1.subdivide(1) + tri2.subdivide(1) + tri3.subdivide(1) + tri4.subdivide(1)
return returnlst
else:
print("Error:level must be 0 or 1")
return


class TriGrid(object):
# private fields: x, y, l, ptlist, lnlist, tblist, hexclist
def __init__(self, x, y, l):
self.x = x
self.y = y
self.l = l
self.ptlist = dict()
self.lnlist = []
self.tblist = []
self.hexclist = []
centroid = Pt(x,y)
self.ptlist[centroid]=0
def grow(self):
pt_selected = random.choice(self.ptlist.keys())
pt1 = Pt(pt_selected.x - self.l, pt_selected.y - sqrt(3) * self.l)
pt2 = Pt(pt_selected.x + self.l, pt_selected.y - sqrt(3) *self.l)
pt3 = Pt(pt_selected.x + 2 * self.l, pt_selected.y)
pt4 = Pt(pt_selected.x + self.l, pt_selected.y + sqrt(3) * self.l)
pt5 = Pt(pt_selected.x - self.l, pt_selected.y + sqrt(3) * self.l)
pt6 = Pt(pt_selected.x - 2 * self.l, pt_selected.y)
pt_add = []
pt_add.append(pt_selected)
pt_add.append(pt1)
pt_add.append(pt2);
pt_add.append(pt3);
pt_add.append(pt4);
pt_add.append(pt5);
pt_add.append(pt6);
ln_add = []
ln1 = Ln(pt_selected, pt1)
ln2 = Ln(pt_selected, pt2)
ln3 = Ln(pt_selected, pt3)
ln4 = Ln(pt_selected, pt4)
ln5 = Ln(pt_selected, pt5)
ln6 = Ln(pt_selected, pt6)
ln_add.append(ln1)
ln_add.append(ln2)
ln_add.append(ln3)
ln_add.append(ln4)
ln_add.append(ln5)
ln_add.append(ln6)
for my_pt in pt_add:
check = True
for v in self.ptlist.keys():
if (v.distance(my_pt) < tolerance):
check = False
# print("duplicate!!!!")
if (check):
self.ptlist[my_pt]=0
# print("added!")
for my_ln in ln_add:
if my_ln not in self.lnlist:
self.lnlist.append(my_ln)
my_lst = []
my_lst.append(Ln(pt1, pt2))
my_lst.append(Ln(pt2, pt3))
my_lst.append(Ln(pt3, pt4))
my_lst.append(Ln(pt4, pt5))
my_lst.append(Ln(pt5, pt6))
my_lst.append(Ln(pt6, pt1))

triblock_add = []
for i in range(len(my_lst)):
if (my_lst[i] in self.lnlist):
if(i==len(my_lst)-1):
triblock_add.append(TriBlock(ln_add[i], ln_add[0], my_lst[i]))
else:
triblock_add.append(TriBlock(ln_add[i], ln_add[i+1], my_lst[i]))

for tb in triblock_add:
if (tb not in self.tblist):
self.tblist.append(tb)
# increment the degree of corresponding vertex in ptlist
for v in tb.vlist:
if (v in self.ptlist):
self.ptlist[v] += 1
else:
for my_v in self.ptlist:
if (v.distance(my_v) < 10**(-1)):
self.ptlist[my_v] += 1

for v in self.ptlist:
if self.ptlist[v] == 6:
self.hexclist.append(v)
global counter, my_trigrid, tolerance
counter = 0
my_trigrid = TriGrid(400,400,30)
tolerance = 10**(-8)
def setup():
frameRate(8)
size (800,800)
# noLoop();
def draw():
background (170,212,234) #BG blue
title()
stroke (255,255,255)
strokeWeight(2)
strokeCap(SQUARE)
for ln in my_trigrid.lnlist:
# the gird
# stroke(100,100,100)
# strokeWeight(1)
stroke(255,255,255)
strokeWeight(3)
line(ln.pt1.x, ln.pt1.y, ln.pt2.x, ln.pt2.y)

for my_tri in my_trigrid.tblist:
# the block
num = random.choice([0,1,2])
for subdived_tri in my_tri.subdivide(num):
if(num==0):
tri = subdived_tri.offset(10)
elif(num==1):
tri = subdived_tri.offset(5)
else:
tri = subdived_tri.offset(3)
fill(240,190,randint(40,140)) #orange blocks colors
stroke(255,255,255)
strokeWeight(1)
triangle(tri.ln1.pt1.x, tri.ln1.pt1.y, tri.ln1.pt2.x, tri.ln1.pt2.y, tri.ln2.pt2.x, tri.ln2.pt2.y)
for v in my_trigrid.hexclist:
hexagon(v.x, v.y, 7)

saveFrame("output/hexagon_####.png")
my_trigrid.grow()

def hexagon(x,y,l):
fill (175,250,194)
# fill (163,222,222)
# fill (35,165,165) #cyan color
# stroke (170,212,234)
stroke (255,255,255)
strokeWeight(4)
beginShape()
vertex(x - l, y - sqrt(3) * l);
vertex(x + l, y - sqrt(3) * l);
vertex(x + 2 * l, y);
vertex(x + l, y + sqrt(3) * l);
vertex(x - l, y + sqrt(3) * l);
vertex(x - 2 * l, y);
endShape(CLOSE)
def title():
fill(0,0,0);
textSize(16);
text("Hexagonal City Growth", 10, 30);
def mousePressed():
global counter
if(counter<50):
redraw()
counter +=1

--

--

Yingjun Mou
Data Mining the City

Architecture student minor in computer science. Currently at Columbia University’s Graduate School of Architecture, Planning & Preservation.