• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SpiNNakerManchester / SpiNNMachine / 5689157980

pending completion
5689157980

push

github

web-flow
Merge pull request #217 from SpiNNakerManchester/version

Version

737 of 822 branches covered (89.66%)

Branch coverage included in aggregate %.

286 of 286 new or added lines in 17 files covered. (100.0%)

2065 of 2173 relevant lines covered (95.03%)

0.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.69
/spinn_machine/machine.py
1
# Copyright (c) 2014 The University of Manchester
2
#
3
# Licensed under the Apache License, Version 2.0 (the "License");
4
# you may not use this file except in compliance with the License.
5
# You may obtain a copy of the License at
6
#
7
#     https://www.apache.org/licenses/LICENSE-2.0
8
#
9
# Unless required by applicable law or agreed to in writing, software
10
# distributed under the License is distributed on an "AS IS" BASIS,
11
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
# See the License for the specific language governing permissions and
13
# limitations under the License.
14

15
from collections import Counter
1✔
16
from .exceptions import (
1✔
17
    SpinnMachineAlreadyExistsException, SpinnMachineException)
18
from spinn_machine.data import MachineDataView
1✔
19
from spinn_machine.link_data_objects import FPGALinkData, SpinnakerLinkData
1✔
20
from spinn_utilities.abstract_base import (
1✔
21
    AbstractBase, abstractproperty, abstractmethod)
22
import logging
1✔
23

24
logger = logging.getLogger(__name__)
1✔
25

26

27
class Machine(object, metaclass=AbstractBase):
1✔
28
    """
29
    A representation of a SpiNNaker Machine with a number of Chips.
30
    Machine is also iterable, providing ``((x, y), chip)`` where:
31

32
        * ``x`` is the x-coordinate of a chip,
33
        * ``y`` is the y-coordinate of a chip,
34
        * ``chip`` is the chip with the given ``(x, y)`` coordinates.
35

36
    """
37

38
    # Table of the amount to add to the x and y coordinates to get the
39
    #  coordinates down the given link (0-5)
40
    LINK_ADD_TABLE = [(1, 0), (1, 1), (0, 1), (-1, 0), (-1, -1), (0, -1)]
1✔
41

42
    __slots__ = (
1✔
43
        "_boot_ethernet_address",
44
        # A map off the expected x, y coordinates on a standard board to
45
        # the most likely number of cores on that chip.
46
        "_chip_core_map",
47
        "_chips",
48
        "_ethernet_connected_chips",
49
        "_fpga_links",
50
        # Declared height of the machine
51
        # This can not be changed
52
        "_height",
53
        # A Counter of the number of cores on each Chip
54
        "_n_cores_counter",
55
        # A Counter of links on each Chip
56
        # Counts each direction so the n_links is half the total
57
        "_n_links_counter",
58
        # A Counter for the number of router entries on each Chip
59
        "_n_router_entries_counter",
60
        # Extra information about how this machine was created
61
        # to be used in the str method
62
        "_origin",
63
        "_spinnaker_links",
64
        # A Counter for sdram on each Chip
65
        "_sdram_counter",
66
        # Declared width of the machine
67
        # This can not be changed
68
        "_width"
69
    )
70

71
    def __init__(self, width, height, chip_core_map, origin=None):
1✔
72
        """
73
        :param int width: The width of the machine excluding
74
        :param int height:
75
            The height of the machine
76
        :param dict((int, int), int) chip_core_map:
77
            A map off the expected x,y coordinates on a standard board to
78
            the most likely number of cores on that chip.
79
        :param str origin: Extra information about how this machine was created
80
            to be used in the str method. Example "``Virtual``" or "``Json``"
81
        :raise SpinnMachineAlreadyExistsException:
82
            If any two chips have the same x and y coordinates
83
        """
84
        if origin is not None:
1✔
85
            assert isinstance(origin, str)
1✔
86
        self._width = width
1✔
87
        self._height = height
1✔
88
        self._chip_core_map = chip_core_map
1✔
89

90
        # The list of chips with Ethernet connections
91
        self._ethernet_connected_chips = list()
1✔
92

93
        # The dictionary of SpiNNaker links by board address and "ID" (int)
94
        self._spinnaker_links = dict()
1✔
95

96
        # The dictionary of FPGA links by board address, FPGA and link ID
97
        self._fpga_links = dict()
1✔
98

99
        # Store the boot chip information
100
        self._boot_ethernet_address = None
1✔
101

102
        # The dictionary of chips
103
        self._chips = dict()
1✔
104

105
        if origin is None:
1✔
106
            self._origin = ""
1✔
107
        else:
108
            self._origin = origin
1✔
109

110
        self._n_cores_counter = Counter()
1✔
111
        self._n_links_counter = Counter()
1✔
112
        self._n_router_entries_counter = Counter()
1✔
113
        self._sdram_counter = Counter()
1✔
114

115
    @abstractmethod
1✔
116
    def multiple_48_chip_boards(self):
1✔
117
        """
118
        Checks that the width and height correspond to the expected size for a
119
        multi-board machine made up of more than one 48 chip board.
120

121
        The assumption is that any size machine can be supported but that
122
        only ones with an expected 48 chip board size can have more than one
123
        Ethernet-enabled chip.
124

125
        :return: True if this machine can have multiple 48 chip boards
126
        :rtype: bool
127
        """
128

129
    @abstractmethod
1✔
130
    def get_xys_by_ethernet(self, ethernet_x, ethernet_y):
1✔
131
        """
132
        Yields the potential x,y locations of all the chips on the board
133
        with this Ethernet-enabled chip. Including the Ethernet-enabled chip
134
        itself.
135

136
        Wrap-arounds are handled as appropriate.
137

138
        .. note::
139
            This method does not check if the chip actually exists as is
140
            intended to be called to create the chips.
141

142
        .. warning::
143
            GIGO! This methods assumes that ethernet_x and ethernet_y are the
144
            local 0,0 of an existing board, within the width and height of the
145
            machine.
146

147
        :param int ethernet_x:
148
            The X coordinate of a (local 0,0) legal Ethernet-enabled chip
149
        :param int ethernet_y:
150
            The Y coordinate of a (local 0,0) legal Ethernet-enabled chip
151
        :return: Yields the (x, y) coordinates of all the potential chips on
152
            this board.
153
        :rtype: iterable(tuple(int,int))
154
        """
155

156
    @abstractmethod
1✔
157
    def get_xy_cores_by_ethernet(self, ethernet_x, ethernet_y):
1✔
158
        """
159
        Yields the potential (x,y) locations and the typical number of cores
160
        of all the chips on the board with this Ethernet-enabled chip.
161

162
        Includes the Ethernet-enabled chip itself.
163

164
        Wrap-arounds are handled as appropriate.
165

166
        .. note::
167
            This method does not check if the chip actually exists,
168
            nor report the actual number of cores on this chip, as is
169
            intended to be called to create the chips.
170

171
        The number of cores is based on the 1,000,000 core machine where the
172
        board where built with the the 17 core chips placed in the same
173
        location on nearly every board.
174

175
        .. warning::
176
            GIGO! This methods assumes that ethernet_x and ethernet_y are the
177
            local 0,0 of an existing board, within the width and height of the
178
            machine.
179

180
        :param int ethernet_x:
181
            The X coordinate of a (local 0,0) legal Ethernet-enabled chip
182
        :param int ethernet_y:
183
            The Y coordinate of a (local 0,0) legal Ethernet-enabled chip
184
        :return: Yields (x, y, n_cores) where x , y are coordinates of all
185
            the potential chips on this board, and n_cores is the typical
186
            number of cores for a chip in that position.
187
        :rtype: iterable(tuple(int,int))
188
        """
189

190
    @abstractmethod
1✔
191
    def get_down_xys_by_ethernet(self, ethernet_x, ethernet_y):
1✔
192
        """
193
        Yields the (x,y) coordinates of the down chips on the board with this
194
        Ethernet-enabled chip.
195

196
        .. note::
197
            The Ethernet chip itself can not be missing if validated.
198

199
        Wrap-arounds are handled as appropriate.
200

201
        This method does check if the chip actually exists.
202

203
        :param int ethernet_x:
204
            The X coordinate of a (local 0,0) legal Ethernet-enabled chip
205
        :param int ethernet_y:
206
            The Y coordinate of a (local 0,0) legal Ethernet-enabled chip
207
        :return: Yields the (x, y) of the down chips on this board.
208
        :rtype: iterable(tuple(int,int))
209
        """
210

211
    def get_chips_by_ethernet(self, ethernet_x, ethernet_y):
1✔
212
        """
213
        Yields the actual chips on the board with this Ethernet-enabled chip.
214
        Including the Ethernet-enabled chip itself.
215

216
        Wrap-arounds are handled as appropriate.
217

218
        This method does check if the chip actually exists.
219

220
        :param int ethernet_x:
221
            The X coordinate of a (local 0,0) legal Ethernet-enabled chip
222
        :param int ethernet_y:
223
            The Y coordinate of a (local 0,0) legal Ethernet-enabled chip
224
        :return: Yields the chips on this board.
225
        :rtype: iterable(Chip)
226
        """
227
        for chip_xy in self.get_existing_xys_by_ethernet(
1✔
228
                ethernet_x, ethernet_y):
229
            yield self._chips[chip_xy]
1✔
230

231
    @abstractmethod
1✔
232
    def get_existing_xys_by_ethernet(self, ethernet_x, ethernet_y):
1✔
233
        """
234
        Yields the (x,y)s of actual chips on the board with this
235
        Ethernet-enabled chip.
236
        Including the Ethernet-enabled chip itself.
237

238
        Wrap-arounds are handled as appropriate.
239

240
        This method does check if the chip actually exists.
241

242
        :param int ethernet_x:
243
            The X coordinate of a (local 0,0) legal Ethernet-enabled chip
244
        :param int ethernet_y:
245
            The Y coordinate of a (local 0,0) legal Ethernet-enabled chip
246
        :return: Yields the (x,y)s of chips on this board.
247
        :rtype: iterable(tuple(int,int))
248
        """
249

250
    @abstractmethod
1✔
251
    def xy_over_link(self, x, y, link):
1✔
252
        """
253
        Get the potential (x,y) location of the chip reached over this link.
254

255
        Wrap-arounds are handled as appropriate.
256

257
        .. note::
258
            This method does not check if either chip source or destination
259
            actually exists as is intended to be called to create the links.
260

261
            It is the callers responsibility to check the validity of this call
262
            before making it or the validity of the result.
263

264
        .. warning::
265
            GIGO! This methods assumes that x and y are within the width and
266
            height of the machine, and that the link goes to another chip on
267
            the machine.
268

269
        On machine without full wrap-around it is possible that this method
270
        generates (x,y) values that fall outside of the legal values including
271
        negative values, x = width or y = height.
272

273
        :param int x: The x coordinate of a chip that will exist on the machine
274
        :param int y: The y coordinate of a chip that will exist on the machine
275
        :param int link:
276
            The link to another chip that could exist on the machine
277
        :return: x and y coordinates of the chip over that link if it is
278
            valid or some fictional (x,y) if not.
279
        :rtype: tuple(int,int)
280
        """
281

282
    @abstractmethod
1✔
283
    def get_local_xy(self, chip):
1✔
284
        """
285
        Converts the x and y coordinates into the local coordinates on the
286
        board as if the Ethernet-enabled chip was at position 0,0.
287

288
        This method does take wrap-arounds into consideration.
289

290
        This method assumes that chip is on the machine or is a copy of a
291
        chip on the machine
292

293
        :param Chip chip: A Chip in the machine
294
        :return: Local (x, y) coordinates.
295
        :rtype: tuple(int,int)
296
        """
297

298
    def where_is_chip(self, chip):
1✔
299
        """
300
        Returns global and local location for this chip.
301

302
        This method assumes that chip is on the machine or is a copy of a
303
        chip on the machine.
304

305
        :param Chip chip: A Chip in the machine
306
        :return: A human-readable description of the location of a chip.
307
        :rtype: str
308
        """
309
        chip00 = self.get_chip_at(0, 0)
1✔
310
        local00 = self.get_chip_at(
1✔
311
            chip.nearest_ethernet_x, chip.nearest_ethernet_y)
312
        (localx, localy) = self.get_local_xy(chip)
1✔
313
        return (f"global chip {chip.x}, {chip.y} on {chip00.ip_address} "
1✔
314
                f"is chip {localx}, {localy} on {local00.ip_address}")
315

316
    def where_is_xy(self, x, y):
1✔
317
        """
318
        Returns global and local location for this chip.
319

320
        :param int x: X coordinate
321
        :param int y: Y coordinate
322
        :return: A human-readable description of the location of a chip.
323
        :rtype: str
324
        """
325
        chip = self.get_chip_at(x, y)
1✔
326
        if chip:
1✔
327
            return self.where_is_chip(chip)
1✔
328
        return f"No chip {x}, {y} found"
1✔
329

330
    @abstractmethod
1✔
331
    def get_global_xy(self, local_x, local_y, ethernet_x, ethernet_y):
1✔
332
        """
333
        Converts the local (X, Y) coordinates into global (x,y) coordinates,
334
        under the assumption that they are on the board with local (0,0) at
335
        global coordinates (`ethernet_x`, `ethernet_y`).
336

337
        This method does take wrap-arounds into consideration.
338

339
        .. warning::
340
            GIGO: This method does not check if input parameters make sense,
341
            nor does it check if there is a chip at the resulting global (x,y)
342

343
        :param int local_x: A valid local x coordinate for a chip
344
        :param int local_y: A valid local y coordinate for a chip
345
        :param int ethernet_x:
346
            The global Ethernet-enabled chip X coordinate for the board the
347
            chip is on
348
        :param int ethernet_y:
349
            The global Ethernet-enabled chip Y coordinate for the board the
350
            chip is on
351
        :return: global (x,y) coordinates of the chip
352
        :rtype: tuple(int,int)
353
        """
354

355
    @abstractmethod
1✔
356
    def get_vector_length(self, source, destination):
1✔
357
        """
358
        Get the mathematical length of the shortest vector (x, y, z) from
359
        source to destination.
360

361
        Use the same algorithm as vector to find the best x, y pair but then
362
        is optimised to directly calculate length
363

364
        This method does not check if the chips and links it assumes to take
365
        actually exist.
366
        For example long paths along a non-wrapping edge may well travel
367
        through the missing area.
368

369
        This method does take wrap-arounds into consideration as applicable.
370

371
        From https://github.com/project-rig/rig/blob/master/rig/geometry.py
372
        Described in http://jhnet.co.uk/articles/torus_paths
373

374
        On full wrap-around machines (before minimisation) the vectors can have
375
        any of the 4 combinations of positive and negative x and y
376
        The positive one is: `destination - source % dimension`
377
        The negative one is: `positive - dimension`
378
        If source is less than dimension the negative one is the wrap around
379
        If destination is greater than source the positive one wraps
380

381
        One no wrap or part wrap boards the x/y that does not wrap is just
382
        destination - source
383

384
        The length of vectors where both x and y have the same sign will be
385
        `max(abs(x), abs(y))`.  As the z direction can be used in minimisation
386
        The length of vectors where x and y have opposite signs will be
387
        `abs(x)` and `abs(y)` as these are already minimum so z is not used.
388

389
        .. warning::
390
            GIGO: This method does not check if input parameters make sense.
391

392
        :param source: (x,y) coordinates of the source chip
393
        :type source: tuple(int, int)
394
        :param destination: (x,y) coordinates of the destination chip
395
        :type destination: tuple(int, int)
396
        :return: The distance in steps
397
        :rtype: int
398
        """
399

400
    @abstractmethod
1✔
401
    def get_vector(self, source, destination):
1✔
402
        """
403
        Get mathematical shortest vector (x, y, z) from source to destination.
404

405
        This method does not check if the chips and links it assumes to take
406
        actually exist.
407
        For example long paths along a non-wrapping edge may well travel
408
        through the missing area.
409

410
        This method does take wrap-arounds into consideration as applicable.
411

412
        From https://github.com/project-rig/rig/blob/master/rig/geometry.py
413
        Described in http://jhnet.co.uk/articles/torus_paths
414

415
        Use the same algorithm as vector_length
416
        using the best x, y pair as `minimize(x, y, 0)`
417

418
        :param source: (x,y) coordinates of the source chip
419
        :type source: tuple(int, int)
420
        :param destination: (x,y) coordinates of the destination chip
421
        :type destination: tuple(int, int)
422
        :return: The vector
423
        """
424

425
    @abstractmethod
1✔
426
    def concentric_xys(self, radius, start):
1✔
427
        """
428
        A generator that produces coordinates for concentric rings of
429
        possible chips based on the links of the chips.
430

431
        No check is done to see if the chip exists.
432
        This may even produce coordinates with negative numbers
433

434
        Mostly copied from:
435
        https://github.com/project-rig/rig/blob/master/rig/geometry.py
436

437
        :param int radius: The radius of rings to produce (0 = start only)
438
        :param tuple(int,int) start: The start coordinate
439
        :rtype: tuple(int,int)
440
        """
441

442
    def validate(self):
1✔
443
        """
444
        Validates the machine and raises an exception in unexpected conditions.
445

446
        Assumes that at the time this is called all chips are on the board.
447

448
        This allows the checks to be avoided when creating a virtual machine
449
        (Except of course in testing)
450

451
        :raises SpinnMachineException:
452
            * An Error is raised if there is a chip with a x outside of the
453
              range 0 to width -1.
454
            * An Error is raised if there is a chip with a y outside of the
455
              range 0 to height -1.
456
            * An Error is raise if there is no chip at the declared
457
              Ethernet-enabled chip x and y.
458
            * An Error is raised if an Ethernet-enabled chip is not at a local
459
              0,0.
460
            * An Error is raised if there is no Ethernet-enabled chip is at
461
              0,0.
462
            * An Error is raised if this is a unexpected multiple board
463
              situation.
464
        """
465
        if self._boot_ethernet_address is None:
1✔
466
            raise SpinnMachineException(
1✔
467
                "no ethernet chip at 0, 0 found")
468
        if len(self._ethernet_connected_chips) > 1:
1✔
469
            if not self.multiple_48_chip_boards():
1!
470
                raise SpinnMachineException(
×
471
                    f"A {self.wrap} machine of size {self._width}, "
472
                    f"{self._height} can not handle multiple ethernet chips")
473
        # The fact that self._boot_ethernet_address is set means there is an
474
        # ethernet chip and it is at 0,0 so no need to check that
475

476
        for chip in self.chips:
1✔
477
            if chip.x < 0:
1!
478
                raise SpinnMachineException(f"{chip} has a negative x")
×
479
            if chip.y < 0:
1!
480
                raise SpinnMachineException(f"{chip} has a negative y")
×
481
            if chip.x >= self._width:
1✔
482
                raise SpinnMachineException(
1✔
483
                    f"{chip} has an x larger than width {self._width}")
484
            if chip.y >= self._height:
1✔
485
                raise SpinnMachineException(
1✔
486
                    f"{chip} has a y larger than height {self._height}")
487
            if chip.ip_address:
1✔
488
                # Ethernet Chip checks
489
                if chip.x % 4 != 0:
1✔
490
                    raise SpinnMachineException(
1✔
491
                        f"Ethernet {chip} has a x which is not divisible by 4")
492
                if (chip.x + chip.y) % 12 != 0:
1!
493
                    raise SpinnMachineException(
×
494
                        f"Ethernet {chip} has an x,y pair that "
495
                        "does not add up to 12")
496
            else:
497
                # Non-Ethernet chip checks
498
                if not self.is_chip_at(
1✔
499
                        chip.nearest_ethernet_x, chip.nearest_ethernet_y):
500
                    raise SpinnMachineException(
1✔
501
                        f"{chip} has an invalid ethernet chip")
502
                local_xy = self.get_local_xy(chip)
1✔
503
                if local_xy not in self._chip_core_map:
1✔
504
                    raise SpinnMachineException(
1✔
505
                        f"{chip} has an unexpected local xy of {local_xy}")
506

507
    @abstractproperty
1✔
508
    def wrap(self):
1✔
509
        """
510
        A short string representing the type of wrap.
511

512
        :rtype: str
513
        """
514

515
    def add_chip(self, chip):
1✔
516
        """
517
        Add a chip to the machine.
518

519
        :param ~spinn_machine.Chip chip: The chip to add to the machine
520
        :raise SpinnMachineAlreadyExistsException:
521
            If a chip with the same x and y coordinates already exists
522
        """
523
        chip_id = (chip.x, chip.y)
1✔
524
        if chip_id in self._chips:
1✔
525
            raise SpinnMachineAlreadyExistsException(
1✔
526
                "chip", f"{chip.x}, {chip.y}")
527

528
        self._chips[chip_id] = chip
1✔
529

530
        # keep some stats about the
531
        self._n_cores_counter[chip.n_processors] += 1
1✔
532
        self._n_links_counter[len(chip.router)] += 1
1✔
533
        self._n_router_entries_counter[
1✔
534
            chip.router.n_available_multicast_entries] += 1
535
        self._sdram_counter[chip.sdram] += 1
1✔
536

537
        if chip.ip_address is not None:
1✔
538
            self._ethernet_connected_chips.append(chip)
1✔
539
            if (chip.x == 0) and (chip.y == 0):
1✔
540
                self._boot_ethernet_address = chip.ip_address
1✔
541

542
    def add_chips(self, chips):
1✔
543
        """
544
        Add some chips to the machine.
545

546
        :param iterable(~spinn_machine.Chip) chips: an iterable of chips
547
        :raise SpinnMachineAlreadyExistsException:
548
            If a chip with the same x and y coordinates as one being added
549
            already exists
550
        """
551
        for next_chip in chips:
×
552
            self.add_chip(next_chip)
×
553

554
    @property
1✔
555
    def chips(self):
1✔
556
        """
557
        An iterable of chips in the machine.
558

559
        :rtype: iterable(:py:class:`~spinn_machine.Chip`)
560
        """
561
        return iter(self._chips.values())
1✔
562

563
    @property
1✔
564
    def chip_coordinates(self):
1✔
565
        """
566
        An iterable of chip coordinates in the machine.
567

568
        :rtype: iterable(tuple(int,int))
569
        """
570
        return iter(self._chips.keys())
1✔
571

572
    def __iter__(self):
1✔
573
        """
574
        Get an iterable of the chip coordinates and chips.
575

576
        :return: An iterable of tuples of ((x, y), chip) where:
577
            * (x, y) is a tuple where:
578
                * x is the x-coordinate of a chip
579
                * y is the y-coordinate of a chip
580
            * chip is a chip
581
        :rtype: iterable(tuple(tuple(int, int), ~spinn_machine.Chip))
582
        """
583
        return iter(self._chips.items())
1✔
584

585
    def __len__(self):
1✔
586
        """
587
        Get the total number of chips.
588

589
        :return: The number of items in the underlying iterable
590
        :rtype: int
591
        """
592
        return len(self._chips)
1✔
593

594
    def get_chip_at(self, x, y):
1✔
595
        """
596
        Get the chip at a specific (x, y) location.
597
        Also implemented as ``__getitem__((x, y))``
598

599
        :param int x: the x-coordinate of the requested chip
600
        :param int y: the y-coordinate of the requested chip
601
        :return: the chip at the specified location,
602
            or ``None`` if no such chip
603
        :rtype: ~spinn_machine.Chip or None
604
        """
605
        chip_id = (x, y)
1✔
606
        if chip_id in self._chips:
1✔
607
            return self._chips[chip_id]
1✔
608
        return None
1✔
609

610
    def __getitem__(self, x_y_tuple):
1✔
611
        """
612
        Get the chip at a specific (x, y) location.
613

614
        :param tuple(int,int) x_y_tuple: A tuple of (x, y) where:
615
            * x is the x-coordinate of the chip to retrieve
616
            * y is the y-coordinate of the chip to retrieve
617
        :return: the chip at the specified location, or `None` if no such chip
618
        :rtype: ~spinn_machine.Chip or None
619
        """
620
        x, y = x_y_tuple
1✔
621
        return self.get_chip_at(x, y)
1✔
622

623
    def is_chip_at(self, x, y):
1✔
624
        """
625
        Determine if a chip exists at the given coordinates.
626
        Also implemented as ``__contains__((x, y))``
627

628
        :param int x: x location of the chip to test for existence
629
        :param int y: y location of the chip to test for existence
630
        :return: True if the chip exists, False otherwise
631
        :rtype: bool
632
        """
633
        return (x, y) in self._chips
1✔
634

635
    def is_link_at(self, x, y, link):
1✔
636
        """
637
        Determine if a link exists at the given coordinates.
638

639
        :param int x: The x location of the chip to test the link of
640
        :param int y: The y location of the chip to test the link of
641
        :param int link: The link to test the existence of
642
        """
643
        return (x, y) in self._chips and self._chips[x, y].router.is_link(link)
1✔
644

645
    def __contains__(self, x_y_tuple):
1✔
646
        """
647
        Determine if a chip exists at the given coordinates.
648

649
        :param x_y_tuple: A tuple of (x, y) where:
650
            * x is the x-coordinate of the chip to retrieve
651
            * y is the y-coordinate of the chip to retrieve
652
        :type x_y_tuple: tuple(int, int)
653
        :return: True if the chip exists, False otherwise
654
        :rtype: bool
655
        """
656
        x, y = x_y_tuple
1✔
657
        return self.is_chip_at(x, y)
1✔
658

659
    @property
1✔
660
    def width(self):
1✔
661
        """
662
        The width of the machine, in chips.
663

664
        :rtype: int
665
        """
666
        return self._width
1✔
667

668
    @property
1✔
669
    def height(self):
1✔
670
        """
671
        The height of the machine, in chips.
672

673
        :rtype: int
674
        """
675
        return self._height
1✔
676

677
    @property
1✔
678
    def n_chips(self):
1✔
679
        """
680
        The number of chips in the machine.
681

682
        :rtype: int
683
        """
684
        return len(self._chips)
1✔
685

686
    @property
1✔
687
    def ethernet_connected_chips(self):
1✔
688
        """
689
        The chips in the machine that have an Ethernet connection.
690

691
        :rtype: iterable(Chip)
692
        """
693
        return self._ethernet_connected_chips
1✔
694

695
    @property
1✔
696
    def spinnaker_links(self):
1✔
697
        """
698
        The set of SpiNNaker links in the machine.
699

700
        :rtype: iterable(tuple(tuple(str,int),
701
            ~spinn_machine.link_data_objects.SpinnakerLinkData))
702
        """
703
        return iter(self._spinnaker_links.items())
1✔
704

705
    def get_spinnaker_link_with_id(
1✔
706
            self, spinnaker_link_id, board_address=None, chip_coords=None):
707
        """
708
        Get a SpiNNaker link with a given ID.
709

710
        :param int spinnaker_link_id: The ID of the link
711
        :param board_address:
712
            optional board address that this SpiNNaker link is associated with.
713
            This is ignored if chip_coords is not `None`.
714
            If this is `None` and chip_coords is `None`,
715
            the boot board will be assumed.
716
        :type board_address: str or None
717
        :param chip_coords:
718
            optional chip coordinates that this SpiNNaker link is associated
719
            with. If this is `None` and board_address is `None`, the boot board
720
            will be assumed.
721
        :type chip_coords: tuple(int, int) or None
722
        :return: The SpiNNaker link data or `None` if no link
723
        :rtype: ~spinn_machine.link_data_objects.SpinnakerLinkData
724
        """
725
        # Try chip coordinates first
726
        if chip_coords is not None:
1!
727
            if board_address is not None:
×
728
                logger.warning(
×
729
                    "Board address will be ignored because chip coordinates"
730
                    " are specified")
731
            if chip_coords not in self._chips:
×
732
                raise KeyError(f"No chip {chip_coords} found!")
×
733
            key = (chip_coords, spinnaker_link_id)
×
734
            link_data = self._spinnaker_links.get(key, None)
×
735
            if link_data is not None:
×
736
                return link_data
×
737
            raise KeyError(
×
738
                f"SpiNNaker link {spinnaker_link_id} not found"
739
                f" on chip {chip_coords}")
740

741
        # Otherwise try board address.
742
        if board_address is None:
1!
743
            board_address = self._boot_ethernet_address
1✔
744
        key = (board_address, spinnaker_link_id)
1✔
745
        if key not in self._spinnaker_links:
1!
746
            raise KeyError(
1✔
747
                f"SpiNNaker Link {spinnaker_link_id} does not exist on board"
748
                f" {board_address}")
749
        return self._spinnaker_links[key]
×
750

751
    def get_fpga_link_with_id(
1✔
752
            self, fpga_id, fpga_link_id, board_address=None, chip_coords=None):
753
        """
754
        Get an FPGA link data item that corresponds to the FPGA and FPGA
755
        link for a given board address.
756

757
        :param int fpga_id:
758
            the ID of the FPGA that the data is going through.  Refer to
759
            technical document located here for more detail:
760
            https://drive.google.com/file/d/0B9312BuJXntlVWowQlJ3RE8wWVE
761
        :param int fpga_link_id:
762
            the link ID of the FPGA. Refer to technical document located here
763
            for more detail:
764
            https://drive.google.com/file/d/0B9312BuJXntlVWowQlJ3RE8wWVE
765
        :param board_address:
766
            optional board address that this FPGA link is associated with.
767
            This is ignored if chip_coords is not `None`.
768
            If this is `None` and chip_coords is `None`, the boot board will be
769
            assumed.
770
        :type board_address: str or None
771
        :param chip_coords:
772
            optional chip coordinates that this FPGA link is associated with.
773
            If this is `None` and board_address is `None`, the boot board
774
            will be assumed.
775
        :type chip_coords: tuple(int, int) or None
776
        :return: the given FPGA link object or ``None`` if no such link
777
        :rtype: ~spinn_machine.link_data_objects.FPGALinkData
778
        """
779
        # Try chip coordinates first
780
        if chip_coords is not None:
1!
781
            if board_address is not None:
×
782
                logger.warning(
×
783
                    "Board address will be ignored because chip coordinates"
784
                    " are specified")
785
            if chip_coords not in self._chips:
×
786
                raise KeyError(f"No chip {chip_coords} found!")
×
787
            key = (chip_coords, fpga_id, fpga_link_id)
×
788
            link_data = self._fpga_links.get(key, None)
×
789
            if link_data is not None:
×
790
                return link_data
×
791
            raise KeyError(
×
792
                f"FPGA {fpga_id}, link {fpga_link_id} not found"
793
                f" on chip {chip_coords}")
794

795
        # Otherwise try board address
796
        if board_address is None:
1✔
797
            board_address = self._boot_ethernet_address
1✔
798
        key = (board_address, fpga_id, fpga_link_id)
1✔
799
        if key not in self._fpga_links:
1✔
800
            raise KeyError(
1✔
801
                f"FPGA Link {fpga_id}:{fpga_link_id} does not exist on board"
802
                f" {board_address}")
803
        return self._fpga_links[key]
1✔
804

805
    def add_spinnaker_links(self):
1✔
806
        """
807
        Add SpiNNaker links that are on a given machine depending on the
808
        version of the board.
809
        """
810
        if self._width == self._height == 2:
1✔
811
            chip_0_0 = self.get_chip_at(0, 0)
1✔
812
            if not chip_0_0.router.is_link(3):
1!
813
                self._add_spinnaker_link(0, 0, 0, 3, chip_0_0.ip_address)
1✔
814
            chip = self.get_chip_at(1, 0)
1✔
815
            if not chip.router.is_link(0):
1!
816
                self._add_spinnaker_link(1, 1, 0, 0, chip_0_0.ip_address)
1✔
817
        elif (self._width == self._height == 8) or \
1!
818
                self.multiple_48_chip_boards():
819
            for chip in self._ethernet_connected_chips:
1✔
820
                if not chip.router.is_link(4):
1✔
821
                    self._add_spinnaker_link(
1✔
822
                        0, chip.x, chip.y, 4, chip.ip_address)
823

824
    def _add_spinnaker_link(
1✔
825
            self, spinnaker_link_id, x, y, link, board_address):
826
        link_data = SpinnakerLinkData(
1✔
827
            spinnaker_link_id, x, y, link, board_address)
828
        self._spinnaker_links[board_address, spinnaker_link_id] = link_data
1✔
829
        self._spinnaker_links[(x, y), spinnaker_link_id] = link_data
1✔
830

831
    def add_fpga_links(self):
1✔
832
        """
833
        Add FPGA links that are on a given machine depending on the
834
        version of the board.
835
        """
836
        if self._width == self._height == 8 or self.multiple_48_chip_boards():
1✔
837

838
            for ethernet_connected_chip in self._ethernet_connected_chips:
1✔
839

840
                # the sides of the hexagonal shape of the board are as follows
841
                #
842
                #
843
                #                 Top
844
                #                 ####
845
                #                #####
846
                #  Top Left     ###### Right
847
                #              #######
848
                #             ########
849
                #             #######
850
                #    Left     ###### Bottom Right
851
                #             #####
852
                #             Bottom
853
                #
854

855
                # handle the first chip
856
                ex = ethernet_connected_chip.x
1✔
857
                ey = ethernet_connected_chip.y
1✔
858
                ip = ethernet_connected_chip.ip_address
1✔
859

860
                # List of x, y, l1, l2, dx, dy where:
861
                #     x = start x
862
                #     y = start y
863
                #     l1 = first link
864
                #     l2 = second link
865
                #     dx = change in x to next
866
                #     dy = change in y to next
867
                chip_links = [(7, 3, 0, 5, -1, -1),  # Bottom Right
1✔
868
                              (4, 0, 4, 5, -1, 0),   # Bottom
869
                              (0, 0, 4, 3, 0, 1),    # Left
870
                              (0, 3, 2, 3, 1, 1),    # Top Left
871
                              (4, 7, 2, 1, 1, 0),    # Top
872
                              (7, 7, 0, 1, 0, -1)]   # Right
873

874
                f = 0
1✔
875
                lk = 0
1✔
876
                for i, (x, y, l1, l2, dx, dy) in enumerate(chip_links):
1✔
877
                    for _ in range(4):
1✔
878
                        fx = (x + ex) % (self._width)
1✔
879
                        fy = (y + ey) % (self._height)
1✔
880
                        self._add_fpga_link(f, lk, fx, fy, l1, ip, ex, ey)
1✔
881
                        f, lk = self._next_fpga_link(f, lk)
1✔
882
                        if i % 2 == 1:
1✔
883
                            x += dx
1✔
884
                            y += dy
1✔
885
                        fx = (x + ex) % (self._width)
1✔
886
                        fy = (y + ey) % (self._height)
1✔
887
                        self._add_fpga_link(f, lk, fx, fy, l2, ip, ex, ey)
1✔
888
                        f, lk = self._next_fpga_link(f, lk)
1✔
889
                        if i % 2 == 0:
1✔
890
                            x += dx
1✔
891
                            y += dy
1✔
892

893
    # pylint: disable=too-many-arguments
894
    def _add_fpga_link(self, fpga_id, fpga_link, x, y, link, board_address,
1✔
895
                       ex, ey):
896
        if self.is_chip_at(x, y):
1✔
897
            link_data = FPGALinkData(
1✔
898
                fpga_link_id=fpga_link, fpga_id=fpga_id,
899
                connected_chip_x=x, connected_chip_y=y,
900
                connected_link=link, board_address=board_address)
901
            self._fpga_links[board_address, fpga_id, fpga_link] = link_data
1✔
902
            # Add for the exact chip coordinates
903
            self._fpga_links[(x, y), fpga_id, fpga_link] = link_data
1✔
904
            # Add for the Ethernet chip coordinates to allow this to work too
905
            self._fpga_links[(ex, ey), fpga_id, fpga_link] = link_data
1✔
906

907
    @staticmethod
1✔
908
    def _next_fpga_link(fpga_id, fpga_link):
1✔
909
        if fpga_link == 15:
1✔
910
            return fpga_id + 1, 0
1✔
911
        return fpga_id, fpga_link + 1
1✔
912

913
    def __str__(self):
1✔
914
        return (f"[{self._origin}{self.wrap}Machine: width={self._width}, "
1✔
915
                f"height={self._height}, n_chips={len(self._chips)}]")
916

917
    def __repr__(self):
1✔
918
        return self.__str__()
1✔
919

920
    def get_cores_count(self):
1✔
921
        """
922
        Get the number of cores from the machine.
923

924
        :return: n_cores
925
        :rtype: int
926
        """
927
        return sum(n * count for n, count in self._n_cores_counter.items())
1✔
928

929
    def get_links_count(self):
1✔
930
        """
931
        Get the number of links from the machine.
932

933
        Links are assumed to be bidirectional so the total links counted is
934
        half of the unidirectional links found.
935

936
        SpiNNaker and FPGA links are not included.
937

938
        :return: n_links
939
        :rtype: int
940
        """
941
        return sum(n * count for n, count in self._n_links_counter.items()) / 2
1✔
942

943
    @property
1✔
944
    def min_n_router_enteries(self):
1✔
945
        """
946
        The minimum number of router_enteries found on any Chip
947

948
        :return: The lowest n router entry found on any Router
949
        :rtype: int
950
        """
951
        return sorted(self._n_router_entries_counter.keys())[-1]
1✔
952

953
    def summary_string(self):
1✔
954
        """
955
        Gets a summary of the Machine and logs warnings for weirdness
956

957
        :return: A String describing the Machine
958
        :raises IndexError: If there are no Chips in the MAchine
959
        :raises AttributeError: If there is no boot chip
960
        """
961
        # pylint: disable=logging-fstring-interpolation
962
        version = MachineDataView.get_machine_version()
1✔
963

964
        sdram = sorted(self._sdram_counter.keys())
1✔
965
        if len(sdram) == 1:
1✔
966
            if sdram[0] != version.max_sdram_per_chip:
1✔
967
                logger.warning(
1✔
968
                    f"The sdram per chip of {sdram[0]} was different to the "
969
                    f"expected value of {version.max_sdram_per_chip} "
970
                    f"for board Version {version.name}")
971
            sdram_st = f"sdram of {sdram[0]} bytes"
1✔
972
        else:
973
            sdram_st = f"sdram of between {sdram[0]} and {sdram[-1]} bytes"
1✔
974
            logger.warning(f"Not all Chips have the same sdram. "
1✔
975
                           f"The counts where {self._sdram_counter}.")
976

977
        routers = sorted(self._n_router_entries_counter.keys())
1✔
978
        if len(routers) == 1:
1✔
979
            if routers[0] != version.n_router_entries:
1✔
980
                logger.warning(
1✔
981
                    f"The number of router entries per chip of {routers[0]} "
982
                    f"was different to the expected value of "
983
                    f"{version.n_router_entries} "
984
                    f"for board Version {version.name}")
985
            routers_st = f"router table of size {routers[0]}"
1✔
986
        else:
987
            routers_st = (f"router table sizes between "
1✔
988
                          f"{routers[0]} and {routers[-1]}")
989
            logger.warning(
1✔
990
                f"Not all Chips had the same n_router_tables. "
991
                f"The counts where {self._n_router_entries_counter}.")
992

993
        cores = sorted(self._n_cores_counter.keys())
1✔
994
        if len(cores) == 1:
1!
995
            cores_st = f" {cores[0]} cores"
×
996
        else:
997
            cores_st = f"between {cores[0]} and {cores[-1]} cores"
1✔
998

999
        links = sorted(self._n_links_counter.keys())
1✔
1000
        if len(links) == 1:
1!
1001
            links_st = f" {links[0]} links."
×
1002
        else:
1003
            links_st = f"between {links[0]} and {links[-1]} links"
1✔
1004

1005
        return (
1✔
1006
            f"Machine on {self.boot_chip.ip_address} "
1007
            f"with {self.n_chips} Chips, {self.get_cores_count()} cores "
1008
            f"and {self.get_links_count()} links. "
1009
            f"Chips have {sdram_st}, {routers_st}, {cores_st} and {links_st}.")
1010

1011
    @property
1✔
1012
    def boot_chip(self):
1✔
1013
        """
1014
        The chip used to boot the machine.
1015

1016
        :rtype: Chip
1017
        """
1018
        return self._chips[0, 0]
1✔
1019

1020
    def get_existing_xys_on_board(self, chip):
1✔
1021
        """
1022
        Get the chips that are on the same board as the given chip.
1023

1024
        :param chip: The chip to find other chips on the same board as
1025
        :return: An iterable of (x, y) coordinates of chips on the same board
1026
        :rtype: iterable(tuple(int,int))
1027
        """
1028
        return self.get_existing_xys_by_ethernet(
1✔
1029
            chip.nearest_ethernet_x, chip.nearest_ethernet_y)
1030

1031
    @property
1✔
1032
    def total_available_user_cores(self):
1✔
1033
        """
1034
        The total number of cores on the machine which are not
1035
        monitor cores.
1036

1037
        :rtype: int
1038
        """
1039
        return sum(chip.n_user_processors for chip in self.chips)
1✔
1040

1041
    @property
1✔
1042
    def total_cores(self):
1✔
1043
        """
1044
        The total number of cores on the machine, including monitors.
1045

1046
        :rtype: int
1047
        """
1048
        return sum(
1✔
1049
            1 for chip in self.chips for _processor in chip.processors)
1050

1051
    def unreachable_outgoing_chips(self):
1✔
1052
        """
1053
        Detects chips that can not reach any of their neighbours.
1054

1055
        Current implementation does *not* deal with group of unreachable chips.
1056

1057
        :return: List (hopefully empty) if the (x,y) coordinates of
1058
            unreachable chips.
1059
        :rtype: list(tuple(int,int))
1060
        """
1061
        removable_coords = list()
1✔
1062
        for (x, y) in self.chip_coordinates:
1✔
1063
            # If no links out of the chip work, remove it
1064
            is_link = False
1✔
1065
            for link in range(6):
1✔
1066
                if self.is_link_at(x, y, link):
1✔
1067
                    is_link = True
1✔
1068
                    break
1✔
1069
            if not is_link:
1✔
1070
                removable_coords.append((x, y))
1✔
1071
        return removable_coords
1✔
1072

1073
    def unreachable_incoming_chips(self):
1✔
1074
        """
1075
        Detects chips that are not reachable from any of their neighbours.
1076

1077
        Current implementation does *not* deal with group of unreachable chips.
1078

1079
        :return: List (hopefully empty) if the (x,y) coordinates of
1080
            unreachable chips.
1081
        :rtype: list(tuple(int,int))
1082
        """
1083
        removable_coords = list()
1✔
1084
        for (x, y) in self.chip_coordinates:
1✔
1085
            # Go through all the chips that surround this one
1086
            moves = [(1, 0), (1, 1), (0, 1), (-1, 0), (-1, -1), (0, -1)]
1✔
1087
            is_link = False
1✔
1088
            for link, (x_move, y_move) in enumerate(moves):
1✔
1089
                opposite = (link + 3) % 6
1✔
1090
                next_x = x + x_move
1✔
1091
                next_y = y + y_move
1✔
1092
                if self.is_link_at(next_x, next_y, opposite):
1✔
1093
                    is_link = True
1✔
1094
                    break
1✔
1095
            if not is_link:
1✔
1096
                removable_coords.append((x, y))
1✔
1097
        return removable_coords
1✔
1098

1099
    def unreachable_outgoing_local_chips(self):
1✔
1100
        """
1101
        Detects chips that can not reach any of their *local* neighbours.
1102

1103
        Current implementation does *not* deal with group of unreachable chips.
1104

1105
        :return: List (hopefully empty) if the (x,y) coordinates of
1106
            unreachable chips.
1107
        :rtype: list(tuple(int,int))
1108
        """
1109
        removable_coords = list()
1✔
1110
        for chip in self._chips.values():
1✔
1111
            # If no links out of the chip work, remove it
1112
            is_link = False
1✔
1113
            moves = [(1, 0), (1, 1), (0, 1), (-1, 0), (-1, -1), (0, -1)]
1✔
1114
            x = chip.x
1✔
1115
            y = chip.y
1✔
1116
            nearest_ethernet_x = chip.nearest_ethernet_x
1✔
1117
            nearest_ethernet_y = chip.nearest_ethernet_y
1✔
1118
            for link, (x_move, y_move) in enumerate(moves):
1✔
1119
                if chip.router.is_link(link):
1✔
1120
                    n_x_y = (x + x_move, y + y_move)
1✔
1121
                    if n_x_y in self._chips:
1✔
1122
                        neighbour = self._chips[n_x_y]
1✔
1123
                        if (neighbour.nearest_ethernet_x ==
1✔
1124
                                nearest_ethernet_x and
1125
                            neighbour.nearest_ethernet_y ==
1126
                                nearest_ethernet_y):
1127
                            is_link = True
1✔
1128
                            break
1✔
1129
            if not is_link:
1✔
1130
                removable_coords.append((x, y))
1✔
1131
        return removable_coords
1✔
1132

1133
    def unreachable_incoming_local_chips(self):
1✔
1134
        """
1135
        Detects chips that are not reachable from any of their *local*
1136
        neighbours.
1137

1138
        Current implementation does *not* deal with group of unreachable chips.
1139

1140
        :return: List (hopefully empty) if the (x,y) coordinates of
1141
            unreachable chips.
1142
        :rtype: list(tuple(int,int))
1143
        """
1144
        removable_coords = list()
1✔
1145
        for chip in self._chips.values():
1✔
1146
            x = chip.x
1✔
1147
            y = chip.y
1✔
1148
            nearest_ethernet_x = chip.nearest_ethernet_x
1✔
1149
            nearest_ethernet_y = chip.nearest_ethernet_y
1✔
1150
            # Go through all the chips that surround this one
1151
            moves = [(-1, 0), (-1, -1), (0, -1), (1, 0), (1, 1), (0, 1)]
1✔
1152
            is_link = False
1✔
1153
            for opposite, (x_move, y_move) in enumerate(moves):
1✔
1154
                n_x_y = (x + x_move, y + y_move)
1✔
1155
                if n_x_y in self._chips:
1✔
1156
                    neighbour = self._chips[n_x_y]
1✔
1157
                    if neighbour.router.is_link(opposite):
1✔
1158
                        if (neighbour.nearest_ethernet_x ==
1✔
1159
                                nearest_ethernet_x and
1160
                            neighbour.nearest_ethernet_y ==
1161
                                nearest_ethernet_y):
1162
                            is_link = True
1✔
1163
                            break
1✔
1164
            if not is_link:
1✔
1165
                removable_coords.append((x, y))
1✔
1166
        return removable_coords
1✔
1167

1168
    def one_way_links(self):
1✔
1169
        """
1170
        :rtype: iterable(tuple(int,int,int))
1171
        """
1172
        link_checks = [(0, 3), (1, 4), (2, 5), (3, 0), (4, 1), (5, 2)]
1✔
1173
        for chip in self.chips:
1✔
1174
            for out, back in link_checks:
1✔
1175
                link = chip.router.get_link(out)
1✔
1176
                if link is not None:
1✔
1177
                    if not self.is_link_at(
1✔
1178
                            link.destination_x, link.destination_y, back):
1179
                        yield chip.x, chip.y, out, back
1✔
1180

1181
    def _minimize_vector(self, x, y):
1✔
1182
        """
1183
        Minimizes an (x, y, 0) vector.
1184

1185
        When vectors are minimised, (1,1,1) is added or subtracted from them.
1186
        This process does not change the range of numbers in the vector.
1187
        When a vector is minimal,
1188
        it is easy to see that the range of numbers gives the
1189
        magnitude since there are at most two non-zero numbers (with opposite
1190
        signs) and the sum of their magnitudes will also be their range.
1191

1192
        This can be farther optimised with then knowledge that z is always 0
1193

1194
        :param int x:
1195
        :param int y:
1196
        :return: (x, y, z) vector
1197
        :rtype: tuple(int,int,int)
1198
        """
1199
        if x > 0:
1✔
1200
            if y > 0:
1✔
1201
                # delta is the smaller of x or y
1202
                if x > y:
1✔
1203
                    return (x - y, 0, -y)
1✔
1204
                else:
1205
                    return (0, y - x, -x)
1✔
1206
            else:
1207
                # two non-zero numbers (with opposite signs)
1208
                return (x, y, 0)
1✔
1209
        else:
1210
            if y > 0:
1✔
1211
                # two non-zero numbers (with opposite signs)
1212
                return (x, y, 0)
1✔
1213
            else:
1214
                # delta is the greater (nearest to zero) of x or y
1215
                if x > y:
1✔
1216
                    return (0, y - x, -x)
1✔
1217
                else:
1218
                    return (x - y, 0, -y)
1✔
1219

1220
    @property
1✔
1221
    def local_xys(self):
1✔
1222
        """
1223
        Provides a list of local (x,y) coordinates for a perfect board on this
1224
        machine.
1225

1226
        Local (x,y)s never include wrap-arounds.
1227

1228
        .. note::
1229
            No check is done to see if any board in the machine actually
1230
            has a chip with this local (x, y).
1231

1232
        :rtype: iterable(tuple(int,int))
1233
        """
1234
        return self._chip_core_map.keys()
1✔
1235

1236
    def get_unused_xy(self):
1✔
1237
        """
1238
        Finds an unused (x,y) coordinate on this machine.
1239

1240
        This method will not return an (x,y) of an existing chip
1241

1242
        This method will not return an (x,y) on any existing board even if that
1243
        chip does not exist, i.e., it will not return (x,y) of a known dead
1244
        chip.
1245

1246
        It will however return the same `unused_xy` until a chip is added at
1247
        that location.
1248

1249
        :return: an unused (x,y) coordinate
1250
        :rtype: (int, int)
1251
        """
1252
        # get a set of xys that could be connected to any existing ethernet
1253
        xys_by_ethernet = set()
1✔
1254
        for ethernet in self.ethernet_connected_chips:
1✔
1255
            xys_by_ethernet.update(
1✔
1256
                self.get_xys_by_ethernet(ethernet.x, ethernet.y))
1257
        x = 0
1✔
1258
        while (True):
1259
            for y in range(self.height):
1✔
1260
                xy = (x, y)
1✔
1261
                if xy not in self._chips and xy not in xys_by_ethernet:
1✔
1262
                    return xy
1✔
1263
            x += 1
1✔
1264

1265
    def _basic_concentric_xys(self, radius, start):
1✔
1266
        """
1267
        Generates concentric (x,y)s from start without accounting for wrap
1268
        around or checking if the chip exists.
1269

1270
        :param int radius: The radius of rings to produce (0 = start only)
1271
        :param tuple(int,int) start: The start coordinate
1272
        :rtype: tuple(int,int)
1273
        """
1274
        x, y = start
1✔
1275
        yield (x, y)
1✔
1276
        for r in range(1, radius + 1):
1✔
1277
            # Move to the next layer
1278
            y -= 1
1✔
1279
            # Walk around the chips at this radius
1280
            for dx, dy in [(1, 1), (0, 1), (-1, 0), (-1, -1), (0, -1), (1, 0)]:
1✔
1281
                for _ in range(r):
1✔
1282
                    yield (x, y)
1✔
1283
                    x += dx
1✔
1284
                    y += dy
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc