Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 202 additions & 0 deletions rig/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@

from math import sqrt

import numpy as np

from rig.machine import Links


def to_xyz(xy):
"""Convert a two-tuple (x, y) coordinate into an (x, y, 0) coordinate."""
Expand Down Expand Up @@ -214,3 +218,201 @@ def standard_system_dimensions(num_boards):
# Convert the number of triads into numbers of chips (each triad of boards
# contributes as 12x12 block of chips).
return (w * 12, h * 12)


def spinn5_eth_coords(width, height):
"""Generate a list of board coordinates with Ethernet connectivity in a
SpiNNaker machine.

Specifically, generates the coordinates for the Ethernet connected chips of
SpiNN-5 boards arranged in a standard torus topology.

Parameters
----------
width : int
Width of the system in chips.
height : int
Height of the system in chips.
"""
# Internally, work with the width and height rounded up to the next
# multiple of 12
w = ((width + 11) // 12) * 12
h = ((height + 11) // 12) * 12

for x in range(0, w, 12):
for y in range(0, h, 12):
for dx, dy in ((0, 0), (4, 8), (8, 4)):
nx = (x + dx) % w
ny = (y + dy) % h
# Skip points which are outside the range available
if nx < width and ny < height:
yield (nx, ny)


def spinn5_local_eth_coord(x, y, w, h):
"""Get the coordinates of a chip's local ethernet connected chip.

Returns the coordinates of the ethernet connected chip on the same board as
the supplied chip.

.. note::
This function assumes the system is constructed from SpiNN-5 boards

Parameters
----------
x : int
y : int
w : int
Width of the system in chips.
h : int
Height of the system in chips.
"""
dx, dy = SPINN5_ETH_OFFSET[y % 12][x % 12]
return ((x + dx) % w), ((y + dy) % h)


SPINN5_ETH_OFFSET = np.array([
[(vx - x, vy - y) for x, (vx, vy) in enumerate(row)]
for y, row in enumerate([
# Below is an enumeration of the absolute coordinates of the nearest
# ethernet connected chip. Note that the above list comprehension
# changes these into offsets to the nearest chip.
# X: 0 1 2 3 4 5 6 7 8 9 10 11 # noqa Y:
[(+0, +0), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+4, -4), (+4, -4), (+4, -4), (+4, -4), (+4, -4), (+4, -4), (+4, -4)], # noqa 0
[(+0, +0), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+4, -4), (+4, -4), (+4, -4), (+4, -4), (+4, -4), (+4, -4)], # noqa 1
[(+0, +0), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+4, -4), (+4, -4), (+4, -4), (+4, -4), (+4, -4)], # noqa 2
[(+0, +0), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+4, -4), (+4, -4), (+4, -4), (+4, -4)], # noqa 3
[(-4, +4), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+8, +4), (+8, +4), (+8, +4), (+8, +4)], # noqa 4
[(-4, +4), (-4, +4), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+8, +4), (+8, +4), (+8, +4), (+8, +4)], # noqa 5
[(-4, +4), (-4, +4), (-4, +4), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+8, +4), (+8, +4), (+8, +4), (+8, +4)], # noqa 6
[(-4, +4), (-4, +4), (-4, +4), (-4, +4), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+8, +4), (+8, +4), (+8, +4), (+8, +4)], # noqa 7
[(-4, +4), (-4, +4), (-4, +4), (-4, +4), (+4, +8), (+4, +8), (+4, +8), (+4, +8), (+4, +8), (+8, +4), (+8, +4), (+8, +4)], # noqa 8
[(-4, +4), (-4, +4), (-4, +4), (-4, +4), (+4, +8), (+4, +8), (+4, +8), (+4, +8), (+4, +8), (+4, +8), (+8, +4), (+8, +4)], # noqa 9
[(-4, +4), (-4, +4), (-4, +4), (-4, +4), (+4, +8), (+4, +8), (+4, +8), (+4, +8), (+4, +8), (+4, +8), (+4, +8), (+8, +4)], # noqa 10
[(-4, +4), (-4, +4), (-4, +4), (-4, +4), (+4, +8), (+4, +8), (+4, +8), (+4, +8), (+4, +8), (+4, +8), (+4, +8), (+4, +8)] # noqa 11
])
], dtype=int)
"""SpiNN-5 ethernet connected chip lookup.

Used by :py:func:`.spinn5_local_eth_coord`. Given an x and y chip position
modulo 12, return the offset of the board's bottom-left chip from the chip's
position.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just so I understand: (4, 3) -> (0, 0)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So that lookup would return (-4, -3) for the key (4, 3) since the lookup gives the offset of the ethernet connected chip from the current chip. In this case it would indeed point at (0, 0) as the Ethernet connected chip.

I spent so long trying to find a closed-form version of this function and falling back on a LUT was very much a last resort (although the one used by ST and Luis).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, thanks :)


Note: the order of indexes: ``SPINN5_ETH_OFFSET[y][x]``!
"""


def spinn5_chip_coord(x, y):
"""Get the coordinates of a chip on its board.

Given the coordinates of a chip in a multi-board system, calculates the
coordinates of the chip within its board.

.. note::
This function assumes the system is constructed from SpiNN-5 boards

Parameters
----------
x : int
y : int
"""
dx, dy = SPINN5_ETH_OFFSET[y % 12][x % 12]
return (-dx, -dy)


def spinn5_fpga_link(x, y, link):
"""Get the identity of the FPGA link which corresponds with the supplied
link.

.. note::
This function assumes the system is constructed from SpiNN-5 boards
whose FPGAs are loaded with the SpI/O 'spinnaker_fpgas' image.

Parameters
----------
x : int
y : int

Returns
-------
(fpga_num, link_num) or None
If not None, the link supplied passes through an FPGA link. The
returned tuple indicates the FPGA responsible for the sending-side of
the link.

`fpga_num` is the number (0, 1 or 2) of the FPGA responsible for the
link.

`link_num` indicates which of the sixteen SpiNNaker links (0 to 15)
into an FPGA is being used. Links 0-7 are typically handled by S-ATA
link 0 and 8-15 are handled by S-ATA link 1.

Returns None if the supplied link does not pass through an FPGA.
"""
x, y = spinn5_chip_coord(x, y)
return SPINN5_FPGA_LINKS.get((x, y, link))


SPINN5_FPGA_LINKS = {
(0, 0, Links.south_west): (1, 0), # noqa
(0, 0, Links.west): (1, 1), # noqa
(0, 1, Links.south_west): (1, 2), # noqa
(0, 1, Links.west): (1, 3), # noqa
(0, 2, Links.south_west): (1, 4), # noqa
(0, 2, Links.west): (1, 5), # noqa
(0, 3, Links.south_west): (1, 6), # noqa
(0, 3, Links.west): (1, 7), # noqa

(0, 3, Links.north): (1, 8), # noqa
(1, 4, Links.west): (1, 9), # noqa
(1, 4, Links.north): (1, 10), # noqa
(2, 5, Links.west): (1, 11), # noqa
(2, 5, Links.north): (1, 12), # noqa
(3, 6, Links.west): (1, 13), # noqa
(3, 6, Links.north): (1, 14), # noqa
(4, 7, Links.west): (1, 15), # noqa

(4, 7, Links.north): (2, 0), # noqa
(4, 7, Links.north_east): (2, 1), # noqa
(5, 7, Links.north): (2, 2), # noqa
(5, 7, Links.north_east): (2, 3), # noqa
(6, 7, Links.north): (2, 4), # noqa
(6, 7, Links.north_east): (2, 5), # noqa
(7, 7, Links.north): (2, 6), # noqa
(7, 7, Links.north_east): (2, 7), # noqa

(7, 7, Links.east): (2, 8), # noqa
(7, 6, Links.north_east): (2, 9), # noqa
(7, 6, Links.east): (2, 10), # noqa
(7, 5, Links.north_east): (2, 11), # noqa
(7, 5, Links.east): (2, 12), # noqa
(7, 4, Links.north_east): (2, 13), # noqa
(7, 4, Links.east): (2, 14), # noqa
(7, 3, Links.north_east): (2, 15), # noqa

(7, 3, Links.east): (0, 0), # noqa
(7, 3, Links.south): (0, 1), # noqa
(6, 2, Links.east): (0, 2), # noqa
(6, 2, Links.south): (0, 3), # noqa
(5, 1, Links.east): (0, 4), # noqa
(5, 1, Links.south): (0, 5), # noqa
(4, 0, Links.east): (0, 6), # noqa
(4, 0, Links.south): (0, 7), # noqa

(4, 0, Links.south_west): (0, 8), # noqa
(3, 0, Links.south): (0, 9), # noqa
(3, 0, Links.south_west): (0, 10), # noqa
(2, 0, Links.south): (0, 11), # noqa
(2, 0, Links.south_west): (0, 12), # noqa
(1, 0, Links.south): (0, 13), # noqa
(1, 0, Links.south_west): (0, 14), # noqa
(0, 0, Links.south): (0, 15), # noqa
}
"""FPGA link IDs for each link leaving a SpiNN-5 board.

Format::

{(x, y, link): (fpga_num, link_num), ...}

Used by :py:func:`.spinn5_fpga_link`.
"""
138 changes: 137 additions & 1 deletion tests/test_geometry.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import pytest

from rig.machine import Links

from rig.geometry import concentric_hexagons, to_xyz, minimise_xyz, \
shortest_mesh_path_length, shortest_mesh_path, \
shortest_torus_path_length, shortest_torus_path, \
standard_system_dimensions
standard_system_dimensions, spinn5_eth_coords, spinn5_local_eth_coord, \
spinn5_chip_coord, spinn5_fpga_link


def test_concentric_hexagons():
Expand Down Expand Up @@ -337,3 +340,136 @@ def test_standard_system_dimensions():
assert standard_system_dimensions(3 * 1 * 3) == (36, 12)
assert standard_system_dimensions(3 * 2 * 4) == (48, 24)
assert standard_system_dimensions(3 * 1 * 17) == (204, 12)


def test_spinn5_eth_coords():
# Minimal system
assert set(spinn5_eth_coords(12, 12)) == set([(0, 0), (4, 8), (8, 4)])

# Larger, non-square systems
assert set(spinn5_eth_coords(24, 12)) == set([
(0, 0), (4, 8), (8, 4), (12, 0), (16, 8), (20, 4)])
assert set(spinn5_eth_coords(12, 24)) == set([
(0, 0), (4, 8), (8, 4), (0, 12), (4, 20), (8, 16)])

# Larger square system
assert set(spinn5_eth_coords(24, 24)) == set([
(0, 0), (4, 8), (8, 4),
(12, 0), (16, 8), (20, 4),
(0, 12), (4, 20), (8, 16),
(12, 12), (16, 20), (20, 16)
])

# Subsets for non multiples of 12 (i.e. non-spinn-5 based things)
assert set(spinn5_eth_coords(2, 2)) == set([(0, 0)])
assert set(spinn5_eth_coords(8, 8)) == set([(0, 0)])


def test_spinn5_local_eth_coord():
# Points lie on actual eth chips
assert spinn5_local_eth_coord(0, 0, 12, 12) == (0, 0)
assert spinn5_local_eth_coord(4, 8, 12, 12) == (4, 8)
assert spinn5_local_eth_coord(8, 4, 12, 12) == (8, 4)

assert spinn5_local_eth_coord(12, 0, 24, 12) == (12, 0)
assert spinn5_local_eth_coord(16, 8, 24, 12) == (16, 8)
assert spinn5_local_eth_coord(20, 4, 24, 12) == (20, 4)

assert spinn5_local_eth_coord(0, 12, 12, 24) == (0, 12)
assert spinn5_local_eth_coord(8, 16, 12, 24) == (8, 16)
assert spinn5_local_eth_coord(4, 20, 12, 24) == (4, 20)

assert spinn5_local_eth_coord(12, 12, 24, 24) == (12, 12)
assert spinn5_local_eth_coord(16, 20, 24, 24) == (16, 20)
assert spinn5_local_eth_coord(20, 16, 24, 24) == (20, 16)

# Exhaustive check for a 12x12 system
cases = [
# X: 0 1 2 3 4 5 6 7 8 9 10 11 # noqa Y:
[(+0, +0), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+4, +8), (+4, +8), (+4, +8), (+4, +8), (+4, +8), (+4, +8), (+4, +8)], # noqa 0
[(+0, +0), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+4, +8), (+4, +8), (+4, +8), (+4, +8), (+4, +8), (+4, +8)], # noqa 1
[(+0, +0), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+4, +8), (+4, +8), (+4, +8), (+4, +8), (+4, +8)], # noqa 2
[(+0, +0), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+4, +8), (+4, +8), (+4, +8), (+4, +8)], # noqa 3
[(+8, +4), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+8, +4), (+8, +4), (+8, +4), (+8, +4)], # noqa 4
[(+8, +4), (+8, +4), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+8, +4), (+8, +4), (+8, +4), (+8, +4)], # noqa 5
[(+8, +4), (+8, +4), (+8, +4), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+8, +4), (+8, +4), (+8, +4), (+8, +4)], # noqa 6
[(+8, +4), (+8, +4), (+8, +4), (+8, +4), (+0, +0), (+0, +0), (+0, +0), (+0, +0), (+8, +4), (+8, +4), (+8, +4), (+8, +4)], # noqa 7
[(+8, +4), (+8, +4), (+8, +4), (+8, +4), (+4, +8), (+4, +8), (+4, +8), (+4, +8), (+4, +8), (+8, +4), (+8, +4), (+8, +4)], # noqa 8
[(+8, +4), (+8, +4), (+8, +4), (+8, +4), (+4, +8), (+4, +8), (+4, +8), (+4, +8), (+4, +8), (+4, +8), (+8, +4), (+8, +4)], # noqa 9
[(+8, +4), (+8, +4), (+8, +4), (+8, +4), (+4, +8), (+4, +8), (+4, +8), (+4, +8), (+4, +8), (+4, +8), (+4, +8), (+8, +4)], # noqa 10
[(+8, +4), (+8, +4), (+8, +4), (+8, +4), (+4, +8), (+4, +8), (+4, +8), (+4, +8), (+4, +8), (+4, +8), (+4, +8), (+4, +8)] # noqa 11
]
for y, row in enumerate(cases):
for x, eth_coord in enumerate(row):
assert spinn5_local_eth_coord(x, y, 12, 12) == eth_coord

# Still works for non multiples of 12
assert spinn5_local_eth_coord(0, 0, 2, 2) == (0, 0)
assert spinn5_local_eth_coord(0, 1, 2, 2) == (0, 0)
assert spinn5_local_eth_coord(1, 0, 2, 2) == (0, 0)
assert spinn5_local_eth_coord(1, 1, 2, 2) == (0, 0)


@pytest.mark.parametrize("dx", [0, 12, 24, 36])
@pytest.mark.parametrize("dy", [0, 12, 24, 36])
def test_spinn5_chip_coord(dx, dy):
# Should work within a board
assert spinn5_chip_coord(0 + dx, 0 + dy) == (0, 0)
assert spinn5_chip_coord(4 + dx, 0 + dy) == (4, 0)
assert spinn5_chip_coord(0 + dx, 3 + dy) == (0, 3)
assert spinn5_chip_coord(7 + dx, 3 + dy) == (7, 3)
assert spinn5_chip_coord(4 + dx, 7 + dy) == (4, 7)
assert spinn5_chip_coord(7 + dx, 7 + dy) == (7, 7)
assert spinn5_chip_coord(4 + dx, 4 + dy) == (4, 4)

# Should work when wrapping around
assert spinn5_chip_coord(5 + dx, 0 + dy) == (1, 4)
assert spinn5_chip_coord(8 + dx, 3 + dy) == (4, 7)
assert spinn5_chip_coord(8 + dx, 4 + dy) == (0, 0)
assert spinn5_chip_coord(8 + dx, 7 + dy) == (0, 3)
assert spinn5_chip_coord(8 + dx, 8 + dy) == (4, 0)
assert spinn5_chip_coord(4 + dx, 8 + dy) == (0, 0)
assert spinn5_chip_coord(3 + dx, 7 + dy) == (7, 3)
assert spinn5_chip_coord(0 + dx, 4 + dy) == (4, 0)


def test_spinn5_fpga_link():
# Check that all outer chips in a SpiNN-5 board are reported as having
# FPGA links.
spinn5_chips = set([ # noqa
(4, 7), (5, 7), (6, 7), (7, 7),
(3, 6), (4, 6), (5, 6), (6, 6), (7, 6),
(2, 5), (3, 5), (4, 5), (5, 5), (6, 5), (7, 5),
(1, 4), (2, 4), (3, 4), (4, 4), (5, 4), (6, 4), (7, 4),
(0, 3), (1, 3), (2, 3), (3, 3), (4, 3), (5, 3), (6, 3), (7, 3),
(0, 2), (1, 2), (2, 2), (3, 2), (4, 2), (5, 2), (6, 2),
(0, 1), (1, 1), (2, 1), (3, 1), (4, 1), (5, 1),
(0, 0), (1, 0), (2, 0), (3, 0), (4, 0),
])
for x, y in spinn5_chips:
for link in Links:
xx = x + link.to_vector()[0]
yy = y + link.to_vector()[1]
if (xx, yy) in spinn5_chips:
assert spinn5_fpga_link(x, y, link) is None
else:
assert spinn5_fpga_link(x, y, link) is not None

# Check a representative subset of the links get the correct numbers
assert spinn5_fpga_link(0, 0, Links.south_west) == (1, 0)
assert spinn5_fpga_link(0, 0, Links.south) == (0, 15)

assert spinn5_fpga_link(0, 3, Links.west) == (1, 7)
assert spinn5_fpga_link(0, 3, Links.north) == (1, 8)

assert spinn5_fpga_link(4, 7, Links.west) == (1, 15)
assert spinn5_fpga_link(4, 7, Links.north) == (2, 0)

assert spinn5_fpga_link(7, 7, Links.north_east) == (2, 7)
assert spinn5_fpga_link(7, 7, Links.east) == (2, 8)

assert spinn5_fpga_link(7, 3, Links.north_east) == (2, 15)
assert spinn5_fpga_link(7, 3, Links.east) == (0, 0)

assert spinn5_fpga_link(4, 0, Links.south) == (0, 7)
assert spinn5_fpga_link(4, 0, Links.south_west) == (0, 8)