Shortest Walk and Flock: Filling Neighborhood Vacancy with Cohesive Retail Districts

Will Cao
Generative Design Course
9 min readMay 8, 2023

Will Cao, Shuhan Liu, Haolan Luo, and E.J. Shin

Introduction

The project aims to explore the potential of connecting new retail owners and public agencies with options to fill vacant storefronts. By considering Euclidean and walking distances between available vacant lots and existing locations of the same retail type, the project can provide a recommendation of which vacant lot could be introduced to best serve both the incoming retail owner and the neighborhood.

Reconsidering vacant storefronts.
Revitalizing streets with appropriate retail.

This solution could be utilized by government agencies to provide recommendations for both public and commercial use, to develop neighborhoods in a much more coherent manner while serving the needs of private retail owners. For example, it could be beneficial to minimize food deserts by distributing grocery stores around a neighborhood, while providing retail owners with access to untapped long-term clientele from niches within the urban fabric.

Splash page of the potential tool.
Prototype user interface of the potential tool.

Literature Review

The program is developed to encourage well-balanced neighborhoods. As studied in the East Village Manhattan Commercial District Needs Assessment, leakage and surplus of businesses can be costly, which this vacant lot optimizer will address.

Retail Leakage and Surplus from the East Village Commercial District Needs Assessment

In 1986, Craig Reynolds simulated behavior of flocking birds through particle swarm optimization. As birds are attracted to a food source, its movement is supposed to be based not only on the vector that brings it closer to the food source, but additionally the vector that brings its neighbor(s) closer to the food source. By considering the needs of the whole in addition to its individual needs, collision is avoided, and many more birds participate in the benefits of the food source.

Flocking birds illustration.

Applying this concept to land use and urban design, prime retail locations are often adjacent to the heavy foot traffic transit stations, a public amenity. Although perhaps not immediately evident, both neighborhoods and retail owners can benefit from the even distribution of retail in a neighborhood, however. Streets are invigorated by active storefronts and sidewalk congestion is reduced, while competition is reduced between retail owners. These two principles, represented as vectors across urban space, guided our model for discovering prime retail locations that would benefit both private retail and owners and public agencies.

Methodology

Diagrammatic workflow of the grasshopper script.

Design Space Model

Using QGIS and Carto, we compiled and cleaned “Storefront Reported Vacant or Not,” “Legally Operating Businesses,” and “Building Footprints” NYC Open Data within the vicinity of the Delancey Street and Essex Street subway station and prepared this information for import into Rhino. Our critical inputs, the locations of vacant lots viable for retail and existing retail by type, originated from this extract.

Initial data preparation in QGIS.

Our base layers of the urban context, originating from the DOITT’s NYC 3D Model by Community District, allowed for the generation of a street network for the shortest walk calculations within the definition. The street centerlines were manually categorized by direction and processed into the input line geometry and intersection points.

Initial data preparation in Carto.

At the start of the grasshopper definition, random vacant lots are selected for the new retail locations. The index and number of locations are Discover inputs.

The full design space in grasshopper.

The first of two circle packing operations generates circles by equal distribution of the area of the “neighborhood.” A hull of the street network was created to represent a “neighborhood” defined by the extents of the input data, and its area was divided by the total number of retail locations of the same type, both new and existing. This first operation begins by iteratively clustering closer to the subway stop while colliding away from the circles of all other retail locations, new and existing.

First circle packing operation in grasshopper, Rhino, and python.
import Rhino.Geometry as rh
import math

class Shop:

# intialize shop instance with center point, radius, and empty list of neighbors
def __init__(self, point, rad):
self.cp = point
self.r = rad
self.neighbors = []

# method for adding another shop object to list of neighbors
def addNeighbor(self, other):
self.neighbors.append(other)

# method for checking distance to other shop object and moving apart if they are colliding
def collide(self, other):

# distance from other shop object
d = self.cp.DistanceTo(other.cp)

# if the distance is LESS than sum of radii
if d < (self.r + other.r):

# find the direction of vector
vec = rh.Vector3d(self.cp) - rh.Vector3d(other.cp)

# find magnitude (difference between actual & desired distance)
overlap = (self.r + other.r) - d

# move circle location that fulfill desired distance between the two
vec.Unitize()
vec = vec * overlap * alpha
self.cp.Transform(rh.Transform.Translation(vec))


# method for checking distance to other shop object and movig closer if they are not touching
def cluster(self, other):

# distance from neighbor
d = self.cp.DistanceTo(other.cp)

# if the distance is GREATER than sum of radii
if d > (self.r + other.r):

# find the direction of vector
vec = rh.Vector3d(self.cp) - rh.Vector3d(other.cp)

# find magnitude (difference between actual & desired distance)
gap = (self.r + other.r) - d

# move circle location that fulfill desired distance between the two
vec.Unitize()
vec = vec * gap * alpha
self.cp.Transform(rh.Transform.Translation(vec))

# create empty lists to store shop objects
new = []
exist = []
shops = []
# radius = math.sqrt( area / math.pi )

# loop over all center points, create new shop object, and add to list of shops
# first list for new and second list for existing
for i, point in enumerate(new_shops):
n = Shop(point, radius)
new.append(n)
shops.append(n)

for i, point in enumerate(exist_shops):
e = Shop(point, radius)
exist.append(e)
shops.append(e)

# subway as object
s = Shop(subway, radius)

# loop over all center points, create new shop object, and add to list of shops
for n in new:
for shop in shops:
if n != shop:
n.addNeighbor(shop)

# LOCAL OPTIMIZATION LOOP
# for several iterations, run cluster for each shop and its neighbors
# and collision on all shop pairs
for i in range(max_iter):
for n in new:

# 1 - cluster
n.cluster(s)

# 2 - collision
for neighbor in n.neighbors:
n.collide(neighbor)


# export list of moved object center points from script
cps = []
for n in new:
cps.append(n.cp)

With approximate viable locations for new retail from the first process, shortest walks are generated between all retail locations of the same type, both new and existing. It is necessary to generate these after the first circle packing operation to get more accurate shortest walk paths between new retail and existing retail, as opposed to the initial randomly selected lots.

Recursive workflow of the GHPython Script.

The shortest walks for each retail location to all neighboring retail locations is used to generate a new set of circles to pack that consider urban rather than Euclidean movement through space, each of which are unique to the retail location. The second of the two circle packing operations more subtly nudges retail locations based on the shortest walk information.

import Rhino.Geometry as rh
import math

class Shop:

# intialize shop instance with center point, radius, and empty list of neighbors
def __init__(self, point, rad):
self.cp = point
self.r = rad
self.neighbors = []

# method for adding another shop object to list of neighbors
def addNeighbor(self, other):
self.neighbors.append(other)

# method for checking distance to other shop object and moving apart if they are colliding
def collide(self, other):

# distance from other shop object
d = self.cp.DistanceTo(other.cp)

# if the distance is LESS than sum of radii
if d < (self.r + other.r):

# find the direction of vector
vec = rh.Vector3d(self.cp) - rh.Vector3d(other.cp)

# find magnitude (difference between actual & desired distance)
overlap = (self.r + other.r) - d

# move circle location that fulfill desired distance between the two
vec.Unitize()
vec = vec * overlap * alpha
self.cp.Transform(rh.Transform.Translation(vec))

# create empty lists to store shop objects
new = []
exist = []
shops = []
# radius = math.sqrt( area / math.pi )

# loop over all center points, create new shop object, and add to list of shops
# first list for new and second list for existing
for i, point in enumerate(new_shops):
n = Shop(point, radii[i])
new.append(n)
shops.append(n)

for i, point in enumerate(exist_shops):
e = Shop(point, radii[len(new)+i])
exist.append(e)
shops.append(e)

# loop over all center points, create new shop object, and add to list of shops
for n in new:
for shop in shops:
if n != shop:
n.addNeighbor(shop)

# LOCAL OPTIMIZATION LOOP
# for several iterations, run cluster for each shop and its neighbors
# and collision on all shop pairs
for i in range(max_iter):
for n in new:

# 1 - collision
for neighbor in n.neighbors:
n.collide(neighbor)

# export list of moved object center points from script
cps = []
new_circles = []
exist_circles = []

# new circles as output
for i, n in enumerate(new):
cps.append(n.cp)
new_circles.append(rh.Circle(n.cp, radii[i]))

# existing circles as output
for i, e in enumerate(exist):
exist_circles.append(rh.Circle(e.cp, radii[len(new)+i]))

Input Parameters

The number of iterations and alphas values from both circle packing operations are considered as six total Discover inputs along with the number and selection of vacant lots at start-up.

  1. Number of new stores
  2. Index values of vacant lots
  3. Number of iterations for first circle packing operation
  4. Alpha value, or speed of movement, of first circle packing operation
  5. Number of iterations for second circle packing operation
  6. Alpha value of second circle packing operation
Discover inputs.

The location data and geometry required includes:

  1. Prepared street network of area as curves.
  2. Point location of vacant lots.
  3. Point location of existing retail of the intended type.
  4. Point location of subway station or other attractor.
Geometry Inputs.

Performance Metrics

The script is connected with Discover to produce generations of mutating design iterations, each learning from the previous generations. The seven output metrics to measure the performance of the iterations are the:

  1. Number of new retail locations,
  2. The total area of the neighborhood hull not covered by the first circle packing operation,
  3. The area of all first circles unified into a single geometry, as a percentage of the area of the neighborhood hull,
  4. The total area of any overlaps from the first circle packing operation,
  5. The average distance of retail location from the subway,
  6. The total area of all circles from the second circle packing operation,
  7. The total area of any overlaps from the second circle packing operation.
Outputs after first circle operations.
Outputs after second circle packing operation.

Conclusions

Chart in Discover.

Several optimized outputs were able to be produced in a relatively short span of time, most likely because of the number of outputs or parameters we introduced to control the design. As the charts from Discover demonstrated, these parameters were relatively diverse, as no two outputs seems to have a correlation with one another.

Design generations.

--

--