3

I need to map a collection onto a grid with an odd width and height (thus it can have a center) in a way that spirals out from the center to an arbitrary size. There's a description here that shows how to spiral inwards through a matrix, but it requires knowing the dimensions of the matrix. I want to spiral outwards for any number of grid tiles given just the index. For instance, given a grid like this:

(-1,-1) (0,-1) (1,-1)
(-1,0) (0,0) (1,0)
(-1,1) (0,1) (1,1)

We have a function f that we hardcode an initial direction of up (negative y) and a spiral direction of counterclockwise such that we can say:

f(0) = (0,0)

f(1) = (0,-1)

f(2) = (-1,-1)

f(3) = (-1,0)

And so on to

f(8) = (1,-1) and f(9) = (1,-2)

Is this even possible to model as a function? Would it have something to do with pi because it's kind of a circle? Every spiral function I can find has curves instead of abrupt whole integer steps. I recall from an algorithms class a couple of years back that clamping stuff to integers sometimes counterintuitively makes computation less tractable.

2 Answers2

3

Like Jean Marie pointed out, the answer is the Ulam (square) spiral. It is a recursive algorithm, I was hoping for a clean O(1) function, but it works! It starts on the right and spirals out clockwise.

The following is in the Python language.

def ulam_spiral_xy(n):
    if n == 1:
        return (0, 0)
    else:
        x, y = ulam_spiral_xy(n-1)
        return (
            x + int(math.sin(math.floor(math.sqrt(4*n-7))*math.pi/2)), 
            y + int(-math.cos(math.floor(math.sqrt(4*n-7))*math.pi/2))
        )

This block of code

array = [
        [21,22,23,24,25],
        [20,7,8,9,10],
        [19,6,1,2,11],
        [18,5,4,3,12],
        [17,16,15,14,13]
]

for i in range(1, 26): coords = ulam_spiral_xy(i) print(array[coords[1] + 2][coords[0] + 2], end = " ")

gives us the expected output of 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25

3

It is possible to write the function $f(n)$ in an $O(1)$ form (provided you assume all arithmetic operations, including the sqrt function, are $O(1)$). The spiral consists of sequences of motion up, left, down, or right in a predictable pattern, so you just have to deduce which sequence of motions the $n$th point is in, how far within that sequence it is, and where that puts it in the coordinate grid.

Here is a non-recursive version of the function $f(n),$ coded in Python 3.

# Find the nth set of coordinates in a spiral through
# lattice points starting at 0,0

from math import sqrt, floor, ceil import numpy as np

Treating the spiral as a sequence of concentric squares

around the origin, return the inradius of the square that

the nth point is on.

def cycle_number(n): x = floor(sqrt(n)) return ceil(x/2)

def first_number_in_cycle(k): return 4k(k-1) + 1

def side_length_in_cycle(k): return 2*k

def point(x,y): return np.array([x,y])

corner_vectors = (point(1,-1),point(-1,-1),point(-1,1),point(1,1)) side_directions = (point(-1,0),point(0,1),point(1,0),point(0,-1))

def f(n): if n == 0: return point(0,0) k = cycle_number(n) distance_from_cycle_start = n - first_number_in_cycle(k) # Identify which of the four sides (straight sections) # of the cycle n is in: side_length = side_length_in_cycle(k) side = floor(distance_from_cycle_start / side_length) distance_along_side = 1 + distance_from_cycle_start % side_length ref_position = k * corner_vectors[side] return ref_position + distance_along_side * side_directions[side]

#-----------------------------------------------------------

Everything below this point is just to test f(n)

Assume x and y coordinates range from -3 to 3 inclusive

data_array = np.array([[0] * 7] * 7) def write_data(p, value): x = p[0] y = p[1] data_array[y + 3, x + 3] = value

def print_data(): for row in range(7): for column in range(7): print(f' {data_array[row][column]:3}', end='') print('')

for n in range(49): p = f(n) write_data(p, n) print_data()

Displaying the output in the same coordinate system used in the question (left-handed Cartesian coordinates with the $y$ axis pointing down and the $x$ axis pointing right), the output looks like this:

  30  29  28  27  26  25  48
  31  12  11  10   9  24  47
  32  13   2   1   8  23  46
  33  14   3   0   7  22  45
  34  15   4   5   6  21  44
  35  16  17  18  19  20  43
  36  37  38  39  40  41  42

As a bonus, if you append the following to the Python code shown above, you get a function sequence_number that takes a pair of coordinates and returns the value of $n$ such that $f(n)$ is the given pair of coordinates. That is, sequence_number is the inverse of f. Using sequence_number, we can produce the same output shown above, but it is computed left to right one row at a time instead of by increasing values of $n.$

def taxicab_distance(u,v):
    return max(abs(v[0] - u[0]), abs(v[1] - u[1]))

Return the value of n such that f(n) has the given coordinates

def sequence_number(p): x = p[0] y = p[1] k = max(x, y, -x, -y) # the cycle number side = (3 if x == k else 2 if y == k else 1 if x == -k else 0) ref_position = k * corner_vectors[side] distance_along_side = taxicab_distance(p, ref_position) side_length = side_length_in_cycle(k) n = first_number_in_cycle(k) + side * side_length + distance_along_side - 1 return n

#-----------------------------------------------------------

Code to test sequence_number(p)

print('plotting f(n) -> n') for y in range(-3, 4): for x in range(-3, 4): p = vector(x,y) n = sequence_number(p) print(f' {n:3}', end='') print('')

David K
  • 108,155