Iterate over 2d array in an expanding circular spiral

Jason Sundram picture Jason Sundram · Jan 23, 2012 · Viewed 10.8k times · Source

Given an n by n matrix M, at row i and column j, I'd like to iterate over all the neighboring values in a circular spiral.

The point of doing this is to test some function, f, which depends on M, to find the radius away from (i, j) in which f returns True. So, f looks like this:

def f(x, y):
    """do stuff with x and y, and return a bool"""

and would be called like this:

R = numpy.zeros(M.shape, dtype=numpy.int)
# for (i, j) in M
for (radius, (cx, cy)) in circle_around(i, j):
    if not f(M[i][j], M[cx][cy]):
       R[cx][cy] = radius - 1
       break

Where circle_around is the function that returns (an iterator to) indices in a circular spiral. So for every point in M, this code would compute and store the radius from that point in which f returns True.

If there's a more efficient way of computing R, I'd be open to that, too.


Update:

Thanks to everyone who submitted answers. I've written a short function to plot the output from your circle_around iterators, to show what they do. If you update your answer or post a new one, you can use this code to validate your solution.

from matplotlib import pyplot as plt
def plot(g, name):
    plt.axis([-10, 10, -10, 10])
    ax = plt.gca()
    ax.yaxis.grid(color='gray')
    ax.xaxis.grid(color='gray')

    X, Y = [], []
    for i in xrange(100):
        (r, (x, y)) = g.next()
        X.append(x)
        Y.append(y)
        print "%d: radius %d" % (i, r)

    plt.plot(X, Y, 'r-', linewidth=2.0)
    plt.title(name)
    plt.savefig(name + ".png")

Here are the results: plot(circle_around(0, 0), "F.J"): circle_around by F.J

plot(circle_around(0, 0, 10), "WolframH"): circle_around by WolframH

I've coded up Magnesium's suggestion as follows:

def circle_around_magnesium(x, y):
    import math
    theta = 0
    dtheta = math.pi / 32.0
    a, b = (0, 1) # are there better params to use here?
    spiral = lambda theta : a + b*theta
    lastX, lastY = (x, y)
    while True:
        r = spiral(theta)
        X = r * math.cos(theta)
        Y = r * math.sin(theta)
        if round(X) != lastX or round(Y) != lastY:
            lastX, lastY = round(X), round(Y)
            yield (r, (lastX, lastY))
        theta += dtheta

plot(circle_around(0, 0, 10), "magnesium"): circle_around by Magnesium

As you can see, none of the results that satisfy the interface I'm looking for have produced a circular spiral that covers all of the indices around 0, 0. F.J's is the closest, although WolframH's hits the right points, just not in spiral order.

Answer

Hooked picture Hooked · Mar 6, 2012

Since it was mentioned that the order of the points do not matter, I've simply ordered them by the angle (arctan2) in which they appear at a given radius. Change N to get more points.

from numpy import *
N = 8

# Find the unique distances
X,Y = meshgrid(arange(N),arange(N))
G = sqrt(X**2+Y**2)
U = unique(G)

# Identify these coordinates
blocks = [[pair for pair in zip(*where(G==idx))] for idx in U if idx<N/2]

# Permute along the different orthogonal directions
directions = array([[1,1],[-1,1],[1,-1],[-1,-1]])

all_R = []
for b in blocks:
    R = set()
    for item in b:
        for x in item*directions:
            R.add(tuple(x))

    R = array(list(R))

    # Sort by angle
    T = array([arctan2(*x) for x in R])
    R = R[argsort(T)]
    all_R.append(R)

# Display the output
from pylab import *
colors = ['r','k','b','y','g']*10
for c,R in zip(colors,all_R):
    X,Y = map(list,zip(*R))

    # Connect last point
    X = X + [X[0],]
    Y = Y + [Y[0],]
    scatter(X,Y,c=c,s=150)
    plot(X,Y,color=c)

axis('equal')
show()

Gives for N=8:

enter image description here

More points N=16 (sorry for the colorblind):

enter image description here

This clearly approaches a circle and hits every grid point in order of increasing radius.

enter image description here