def test_smallest_child_first(self):
        p = PackTree(0, 0, 3, 1)
        p.vsplit(1)
        assert p.alloc_area(1) == (0, 0, 1, 1)

        p = PackTree(0, 0, 3, 1)
        p.vsplit(2)
        assert p.alloc_area(1) == (2, 0, 1, 1)
    def test_smallest_child_first(self):
        p = PackTree(0, 0, 3, 1)
        p.vsplit(1)
        assert p.alloc_area(1) == (0, 0, 1, 1)

        p = PackTree(0, 0, 3, 1)
        p.vsplit(2)
        assert p.alloc_area(1) == (2, 0, 1, 1)
 def test_candidate_filter(self):
     p = PackTree(0, 0, 2, 1)
     p.vsplit(1)
     cf = Mock(side_effect=[False, True])
     assert p.alloc_area(1, 0.0, cf) == (1, 0, 1, 1)
     assert cf.mock_calls == [
         call(0, 0, 1, 1),
         call(1, 0, 1, 1),
     ]
 def test_candidate_filter(self):
     p = PackTree(0, 0, 2, 1)
     p.vsplit(1)
     cf = Mock(side_effect=[False, True])
     assert p.alloc_area(1, 0.0, cf) == (1, 0, 1, 1)
     assert cf.mock_calls == [
         call(0, 0, 1, 1),
         call(1, 0, 1, 1),
     ]
 def test_one_child_full(self):
     p = PackTree(0, 0, 2, 1)
     assert p.alloc(1, 1) == (0, 0)
     assert p.alloc_area(1) == (1, 0, 1, 1)
     p.free(0, 0)
     assert p.alloc_area(1) == (0, 0, 1, 1)
 def test_children_full(self):
     p = PackTree(0, 0, 2, 1)
     assert p.alloc(1, 1) == (0, 0)
     assert p.alloc(1, 1) == (1, 0)
     assert p.alloc_area(1) is None
    def test_no_children(self):
        p = PackTree(0, 0, 3, 3)
        assert p.alloc_area(6) == (0, 0, 3, 2)

        p = PackTree(0, 0, 3, 3)
        assert p.alloc_area(6, 1.0) == (0, 0, 3, 3)
 def test_unsuitable_ratio(self):
     p = PackTree(0, 0, 3, 1)
     assert p.alloc_area(3, min_ratio=1.0) is None
 def test_too_large(self):
     p = PackTree(0, 0, 1, 1)
     assert p.alloc_area(2) is None
 def test_too_large(self):
     p = PackTree(0, 0, 1, 1)
     assert p.alloc_area(2) is None
 def test_one_child_full(self):
     p = PackTree(0, 0, 2, 1)
     assert p.alloc(1, 1) == (0, 0)
     assert p.alloc_area(1) == (1, 0, 1, 1)
     p.free(0, 0)
     assert p.alloc_area(1) == (0, 0, 1, 1)
 def test_children_full(self):
     p = PackTree(0, 0, 2, 1)
     assert p.alloc(1, 1) == (0, 0)
     assert p.alloc(1, 1) == (1, 0)
     assert p.alloc_area(1) is None
    def test_no_children(self):
        p = PackTree(0, 0, 3, 3)
        assert p.alloc_area(6) == (0, 0, 3, 2)

        p = PackTree(0, 0, 3, 3)
        assert p.alloc_area(6, 1.0) == (0, 0, 3, 3)
 def test_unsuitable_ratio(self):
     p = PackTree(0, 0, 3, 1)
     assert p.alloc_area(3, min_ratio=1.0) is None
示例#15
0
 def test_already_allocated(self):
     p = PackTree(0, 0, 1, 1)
     assert p.alloc(1, 1) == (0, 0)
     assert p.alloc_area(1) is None
示例#16
0
class Allocator(object):
    """This object allows high-level allocation of SpiNNaker boards from a
    larger, possibly faulty, toroidal machine.

    Internally this object uses a
    :py:class:`spalloc_server.pack_tree.PackTree` to allocate
    rectangular blocks of triads in a machine. A :py:class:`._CandidateFilter`
    to restrict the allocations made by
    :py:class:`~spalloc_server.pack_tree.PackTree` to those which match
    the needs of the user (e.g. specific connectivity requirements).

    The allocator can allocate either rectangular blocks of triads or
    individual boards. When allocating individual boards, the allocator
    allocates a 1x1 triad block from the
    :py:class:`~spalloc_server.pack_tree.PackTree` and returns one of
    the boards from that block. Subsequent single-board allocations will use up
    spare boards left in triads allocated for single-board allocations before
    allocating new 1x1 triads.
    """

    def __init__(self, width, height, dead_boards=None, dead_links=None,
                 next_id=1):
        """
        Parameters
        ----------
        width, height : int
            Dimensions of the machine in triads.
        dead_boards : set([(x, y, z), ...])
            The set of boards which are dead and which must not be allocated.
        dead_links : set([(x, y, z, :py:class:`rig.links.Links`), ...])
            The set of links leaving boards which are known not to be working.
            Connections to boards in the set dead_boards are assumed to be
            dead and need not be included in this list. Note that both link
            directions must be flagged as dead (if the link is bidirectionally
            down).
        next_id : int
            The ID of the next allocation to be made.
        """
        self.width = width
        self.height = height
        self.dead_boards = dead_boards if dead_boards is not None else set()
        self.dead_links = dead_links if dead_links is not None else set()

        # Unique IDs are assigned to every new allocation. The next ID to be
        # allocated.
        self.next_id = next_id

        # A 2D tree at the granularity of triads used for board allocation.
        self.pack_tree = PackTree(0, 0, width, height)

        # Provides a lookup from (live) allocation IDs to the type of
        # allocation.
        self.allocation_types = {}

        # Lookup from allocation IDs to the bottom-left board in the allocation
        self.allocation_board = {}

        # Since we cannot allocate individual boards in the pack_tree, whenever
        # an individual board is requested a whole triad may be allocated and
        # one of the boards from the triad returned. This dictionary records
        # what triads have been allocated like this and which boards are
        # unused. These may then be used for future single-board allocations
        # rather than allocating another whole triad.

        # A dictionary containing any triads used for allocating individual
        # boards which still have some free and working boards.
        # {(x, y): [z, ...], ...}
        self.single_board_triads = {}

        # When all the boards in a triad in single_board_triads are used up the
        # triad is removed from that dictionary and placed into the set below.
        self.full_single_board_triads = set()

    def _alloc_triads_possible(self, width, height, max_dead_boards=None,
                               max_dead_links=None, require_torus=False,
                               min_ratio=0.0):
        """Is it guaranteed that the specified allocation *could* succeed if
        enough of the machine is free?

        This function may be conservative. If the specified request would fail
        when no resources have been allocated, we return False, even if some
        circumstances the allocation may succeed. For example, if one board in
        each of the four corners of the machine is dead, no allocation with
        max_dead_boards==0 can succeed when the machine is empty but may
        succeed if some other allocation has taken place.

        Parameters
        ----------
        width, height : int
            The size of the block to allocate, in triads.
        max_dead_boards : int or None
            The maximum number of broken or unreachable boards to allow in the
            allocated region. If None, any number of dead boards is permitted,
            as long as the board on the bottom-left corner is alive (Default:
            None).
        max_dead_links : int or None
            The maximum number of broken links allow in the allocated region.
            When require_torus is True this includes wrap-around links,
            otherwise peripheral links are not counted.  If None, any number of
            broken links is allowed. (Default: None).
        require_torus : bool
            If True, only allocate blocks with torus connectivity. In general
            this will only succeed for requests to allocate an entire machine
            (when the machine is otherwise not in use!). (Default: False)
        min_ratio : float
            Ignored.

        Returns
        -------
        bool

        See Also
        --------
        alloc_possible : The (public) wrapper which also supports checking
                         triad allocations.
        """
        # If too big, we can't fit
        if width > self.width or height > self.height:
            return False

        # Can't be a non-machine
        if width <= 0 or height <= 0:
            return False

        # If torus connectivity is required, we must be *exactly* the right
        # size otherwise we can't help...
        if require_torus and (width != self.width or height != self.height):
            return False

        # Test to see whether the allocation could succeed in the idle machine
        cf = _CandidateFilter(self.width, self.height,
                              self.dead_boards, self.dead_links,
                              max_dead_boards, max_dead_links, require_torus)
        for x, y in set([(0, 0),
                         (self.width - width, 0),
                         (0, self.height - height),
                         (self.width - width, self.height - height)]):
            if cf(x, y, width, height):
                return True

        # No possible allocation could be made...
        return False

    def _alloc_triads(self, width, height, max_dead_boards=None,
                      max_dead_links=None, require_torus=False,
                      min_ratio=0.0):
        """Allocate a rectangular block of triads of interconnected boards.

        Parameters
        ----------
        width, height : int
            The size of the block to allocate, in triads.
        max_dead_boards : int or None
            The maximum number of broken or unreachable boards to allow in the
            allocated region. If None, any number of dead boards is permitted,
            as long as the board on the bottom-left corner is alive (Default:
            None).
        max_dead_links : int or None
            The maximum number of broken links allow in the allocated region.
            When require_torus is True this includes wrap-around links,
            otherwise peripheral links are not counted.  If None, any number of
            broken links is allowed. (Default: None).
        require_torus : bool
            If True, only allocate blocks with torus connectivity. In general
            this will only succeed for requests to allocate an entire machine
            (when the machine is otherwise not in use!). (Default: False)
        min_ratio : float
            Ignored.

        Returns
        -------
        (allocation_id, boards, periphery, torus) or None
            If the allocation was successful a four-tuple is returned. If the
            allocation was not successful None is returned.

            The ``allocation_id`` is an integer which should be used to free
            the allocation with the :py:meth:`.free` method. ``boards`` is a
            set of (x, y, z) tuples giving the locations of the (working)
            boards in the allocation. ``periphery`` is a set of (x, y, z, link)
            tuples giving the links which leave the allocated region. ``torus``
            is True iff at least one torus link is working.

        See Also
        --------
        alloc : The (public) wrapper which also supports allocating individual
        boards.
        """
        # Special case: If a torus is required this is only deliverable when
        # the requirements match the size of the machine exactly.
        if require_torus and (width != self.width or height != self.height):
            return None

        # Sanity check: can't be a non-machine
        if width <= 0 or height <= 0:
            return None

        cf = _CandidateFilter(self.width, self.height,
                              self.dead_boards, self.dead_links,
                              max_dead_boards, max_dead_links, require_torus)

        xy = self.pack_tree.alloc(width, height,
                                  candidate_filter=cf)

        # If a block could not be allocated, fail
        if xy is None:
            return None

        # If a block was allocated, store the allocation
        allocation_id = self.next_id
        self.next_id += 1
        self.allocation_types[allocation_id] = _AllocationType.triads
        x, y = xy
        self.allocation_board[allocation_id] = (x, y, 0)

        return (allocation_id, cf.boards, cf.periphery, cf.torus)

    def _alloc_boards_possible(self, boards, min_ratio=0.0,
                               max_dead_boards=None, max_dead_links=None,
                               require_torus=False):
        """Is it guaranteed that the specified allocation *could* succeed if
        enough of the machine is free?

        This function may be conservative. If the specified request would fail
        when no resources have been allocated, we return False, even if some
        circumstances the allocation may succeed. For example, if one board in
        each of the four corners of the machine is dead, no allocation with
        max_dead_boards==0 can succeed when the machine is empty but may
        succeed if some other allocation has taken place.

        Parameters
        ----------
        boards : int
            The *minimum* number of boards, must be at least 1. Note that if
            only 1 board is required, :py:class:`._alloc_board` would be a more
            appropriate function since this function may return as many as
            three boards when only a single one is requested.
        min_ratio : float
            The aspect ratio which the allocated region must be 'at least as
            square as'. Set to 0.0 for any allowable shape.
        max_dead_boards : int or None
            The maximum number of broken or unreachable boards to allow in the
            allocated region. If None, any number of dead boards is permitted,
            as long as the board on the bottom-left corner is alive (Default:
            None).
        max_dead_links : int or None
            The maximum number of broken links allow in the allocated region.
            When require_torus is True this includes wrap-around links,
            otherwise peripheral links are not counted.  If None, any number of
            broken links is allowed. (Default: None).
        require_torus : bool
            If True, only allocate blocks with torus connectivity. In general
            this will only succeed for requests to allocate an entire machine
            (when the machine is otherwise not in use!). (Default: False)

        Returns
        -------
        bool

        See Also
        --------
        alloc_possible : The (public) wrapper which also supports checking
                         triad allocations.
        """
        # Convert number of boards to number of triads (rounding up...)
        triads = int(ceil(boards / 3.0))

        # Sanity check: can't be a non-machine
        if triads <= 0:
            return False

        # Special case: If a torus is required this is only deliverable when
        # the requirements match the size of the machine exactly.
        if require_torus and (triads != self.width * self.height):
            return False

        # If no region of the right shape can be made, just fail
        wh = area_to_rect(triads, self.width, self.height, min_ratio)
        if wh is None:
            return False
        width, height = wh

        # Test to see whether the allocation could succeed in the idle machine
        cf = _CandidateFilter(self.width, self.height,
                              self.dead_boards, self.dead_links,
                              max_dead_boards, max_dead_links, require_torus,
                              boards)
        for x, y in set([(0, 0),
                         (self.width - width, 0),
                         (0, self.height - height),
                         (self.width - width, self.height - height)]):
            if cf(x, y, width, height):
                return True

        # No possible allocation could be made...
        return False

    def _alloc_boards(self, boards, min_ratio=0.0, max_dead_boards=None,
                      max_dead_links=None, require_torus=False):
        """Allocate a rectangular block of triads with at least the specified
        number of boards which is 'at least as square' as the specified aspect
        ratio.

        Parameters
        ----------
        boards : int
            The *minimum* number of boards, must be at least 1. Note that if
            only 1 board is required, :py:class:`._alloc_board` would be a more
            appropriate function since this function may return as many as
            three boards when only a single one is requested.
        min_ratio : float
            The aspect ratio which the allocated region must be 'at least as
            square as'. Set to 0.0 for any allowable shape.
        max_dead_boards : int or None
            The maximum number of broken or unreachable boards to allow in the
            allocated region. If None, any number of dead boards is permitted,
            as long as the board on the bottom-left corner is alive (Default:
            None).
        max_dead_links : int or None
            The maximum number of broken links allow in the allocated region.
            When require_torus is True this includes wrap-around links,
            otherwise peripheral links are not counted.  If None, any number of
            broken links is allowed. (Default: None).
        require_torus : bool
            If True, only allocate blocks with torus connectivity. In general
            this will only succeed for requests to allocate an entire machine
            (when the machine is otherwise not in use!). (Default: False)

        Returns
        -------
        (allocation_id, boards, periphery, torus) or None
            If the allocation was successful a four-tuple is returned. If the
            allocation was not successful None is returned.

            The ``allocation_id`` is an integer which should be used to free
            the allocation with the :py:meth:`.free` method. ``boards`` is a
            set of (x, y, z) tuples giving the locations of the (working)
            boards in the allocation. ``periphery`` is a set of (x, y, z, link)
            tuples giving the links which leave the allocated region. ``torus``
            is a :py:class:`.WrapAround` value indicating torus connectivity
            when at least one torus may exist.

        See Also
        --------
        alloc : The (public) wrapper which also supports allocating individual
        boards.
        """
        # Convert number of boards to number of triads (rounding up...)
        triads = int(ceil(boards / 3.0))

        # Sanity check: can't be a non-machine
        if triads <= 0:
            return None

        # Special case: If a torus is required this is only deliverable when
        # the requirements match the size of the machine exactly.
        if require_torus and (triads != self.width * self.height):
            return None

        cf = _CandidateFilter(self.width, self.height,
                              self.dead_boards, self.dead_links,
                              max_dead_boards, max_dead_links, require_torus,
                              boards)

        xywh = self.pack_tree.alloc_area(triads, min_ratio,
                                         candidate_filter=cf)

        # If a block could not be allocated, fail
        if xywh is None:
            return None

        # If a block was allocated, store the allocation
        allocation_id = self.next_id
        self.next_id += 1
        self.allocation_types[allocation_id] = _AllocationType.triads
        x, y, w, h = xywh
        self.allocation_board[allocation_id] = (x, y, 0)

        return (allocation_id, cf.boards, cf.periphery, cf.torus)

    def _alloc_board_possible(self, x=None, y=None, z=None,
                              max_dead_boards=None, max_dead_links=None,
                              require_torus=False, min_ratio=0.0):
        """Is it guaranteed that the specified allocation *could* succeed if
        enough of the machine is free?

        Parameters
        ----------
        x, y, z : ints or None
            If specified, requests a specific board.
        max_dead_boards : int or None
            Ignored.
        max_dead_links : int or None
            Ignored.
        require_torus : bool
            Must be False.
        min_ratio : float
            Ignored.

        Returns
        -------
        bool

        See Also
        --------
        alloc_possible : The (public) wrapper which also supports checking
                         board allocations.
        """
        assert require_torus is False
        assert (((x is None) == (y is None) == (z is None)) or
                ((x == 1) == (y is None) == (z is None)))

        board_requested = y is not None

        # If the requested board is outside the dimensions of the machine, the
        # request definitely can't be met.
        if board_requested and not(0 <= x < self.width and
                                   0 <= y < self.height and
                                   0 <= z < 3):
            return False

        # If the requested board is dead, this should fail
        if board_requested and (x, y, z) in self.dead_boards:
            return False

        # If there are no working boards, we must also fail
        if len(self.dead_boards) >= (self.width * self.height * 3):
            return False

        # Should be possible!
        return True

    def _alloc_board(self, x=None, y=None, z=None,
                     max_dead_boards=None, max_dead_links=None,
                     require_torus=False, min_ratio=0.0):
        """Allocate a single board, optionally specifying a specific board to
        allocate.

        Parameters
        ----------
        x, y, z : ints or None
            If None, an arbitrary free board will be returned if possible. If
            all are defined, attempts to allocate the specific board requested
            if available and working.
        max_dead_boards : int or None
            Ignored.
        max_dead_links : int or None
            Ignored.
        require_torus : bool
            Must be False.
        min_ratio : float
            Ignored.

        Returns
        -------
        (allocation_id, boards, periphery, torus) or None
            If the allocation was successful a four-tuple is returned. If the
            allocation was not successful None is returned.

            The ``allocation_id`` is an integer which should be used to free
            the allocation with the :py:meth:`.free` method. ``boards`` is a
            set of (x, y, z) tuples giving the location of to allocated board.
            ``periphery`` is a set of (x, y, z, link) tuples giving the links
            which leave the board. ``torus`` is always
            :py:attr:`.WrapAround.none` for single boards.

        See Also
        --------
        alloc : The (public) wrapper which also supports allocating triads.
        """
        assert require_torus is False
        assert (((x is None) == (y is None) == (z is None)) or
                ((x == 1) == (y is None) == (z is None)))

        board_requested = y is not None

        # Special case: the desired board is dead: just give up
        if board_requested and (x, y, z) in self.dead_boards:
            return None

        # Try and return a board from an already allocated set of single-board
        # triads if possible
        if (self.single_board_triads and
                (not board_requested or
                 z in self.single_board_triads.get((x, y), set()))):
            if not board_requested:
                # No specific board requested, return any available
                x, y = next(iter(self.single_board_triads))
                available = self.single_board_triads[(x, y)]
                z = available.pop()
            else:
                # A specific board was requested (and is available), get that
                # one
                available = self.single_board_triads[(x, y)]
                available.remove(z)

            # If we used up the last board, move the traid to the full list
            if not available:
                del self.single_board_triads[(x, y)]
                self.full_single_board_triads.add((x, y))

            # Allocate the board
            allocation_id = self.next_id
            self.next_id += 1
            self.allocation_types[allocation_id] = _AllocationType.board
            self.allocation_board[allocation_id] = (x, y, z)
            return (allocation_id,
                    set([(x, y, z)]),
                    set((x, y, z, link) for link in Links),
                    WrapAround.none)

        # The desired board was not available in an already-allocated triad.
        # Attempt to request that triad.

        def has_at_least_one_working_board(x, y, width, height):
            num_dead = 0
            for z in range(3):
                if (x, y, z) in self.dead_boards:
                    num_dead += 1

            return num_dead < 3

        if board_requested:
            xy = self.pack_tree.request(x, y)
        else:
            xy = self.pack_tree.alloc(
                1, 1, candidate_filter=has_at_least_one_working_board)

        # If a triad could not be allocated, fail
        if xy is None:
            return None

        # If a triad was allocated, add it to the set of allocated triads for
        # single-boards
        self.single_board_triads[xy] = \
            set(z for z in range(3)
                if (xy[0], xy[1], z) not in self.dead_boards)

        # Recursing will return a board from the triad
        return self._alloc_board(x, y, z)

    def _alloc_type(self, x_or_num_or_width=None, y_or_height=None, z=None,
                    max_dead_boards=None, max_dead_links=None,
                    require_torus=False, min_ratio=0.0):
        """Returns the type of allocation the user is attempting to make (and
        fails if it is invalid.

        Usage::

            a.alloc()  # Allocate any single board
            a.alloc(1)  # Allocate any single board
            a.alloc(3, 2, 1)  # Allocate the specific board (3, 2, 1)
            a.alloc(4)  # Allocate at least 4 boards
            a.alloc(2, 3, **kwargs)  # Allocate a 2x3 triad machine

        Parameters
        ----------
        <nothing> OR num OR x, y, z OR width, height
            If nothing, allocate a single board.

            If num, allocate at least that number of boards. Special case: if
            1, allocate exactly 1 board.

            If x, y and z, allocate a specific board.

            If width and height, allocate a block of this size, in triads.
        max_dead_boards : int or None
            The maximum number of broken or unreachable boards to allow in the
            allocated region. If None, any number of dead boards is permitted,
            as long as the board on the bottom-left corner is alive (Default:
            None).
        max_dead_links : int or None
            The maximum number of broken links allow in the allocated region.
            When require_torus is True this includes wrap-around links,
            otherwise peripheral links are not counted.  If None, any number of
            broken links is allowed. (Default: None).
        require_torus : bool
            If True, only allocate blocks with torus connectivity. In general
            this will only succeed for requests to allocate an entire machine
            (when the machine is otherwise not in use!). Must be False when
            allocating boards. (Default: False)

        Returns
        -------
        :py:class:`._AllocationType`
        """
        # Work-around for Python 2's non-support for keyword-only arguments...
        args = []
        if x_or_num_or_width is not None:
            args.append(x_or_num_or_width)
            if y_or_height is not None:
                args.append(y_or_height)
                if z is not None:
                    args.append(z)

        # Select allocation type
        if len(args) == 0:
            alloc_type = _AllocationType.board
        elif len(args) == 1:
            if args[0] == 1:
                alloc_type = _AllocationType.board
            else:
                alloc_type = _AllocationType.boards
        elif len(args) == 2:
            alloc_type = _AllocationType.triads
        elif len(args) == 3:  # pragma: no branch
            alloc_type = _AllocationType.board

        # Validate arguments
        if alloc_type == _AllocationType.board:
            if require_torus:
                raise ValueError(
                    "require_torus must be False when allocating boards.")

        return alloc_type

    def alloc_possible(self, *args, **kwargs):
        """Is the specified allocation actually possible on this machine?

        Usage::

            a.alloc_possible()  # Can allocate a single board?
            a.alloc_possible(1)  # Can allocate a single board?
            a.alloc_possible(4)  # Can allocate at least 4 boards?
            a.alloc_possible(3, 2, 1)  # Can allocate a board (3, 2, 1)?
            a.alloc_possible(2, 3, **kwargs)  # Can allocate 2x3 triads?

        Parameters
        ----------
        <nothing> OR num OR x, y, z OR width, height
            If nothing, allocate a single board.

            If num, allocate at least that number of boards. Special case: if
            1, allocate exactly 1 board.

            If x, y and z, allocate a specific board.

            If width and height, allocate a block of this size, in triads.
        min_ratio : float
            The aspect ratio which the allocated region must be 'at least as
            square as'. Set to 0.0 for any allowable shape. Ignored when
            allocating single boards or specific rectangles of triads.
        max_dead_boards : int or None
            The maximum number of broken or unreachable boards to allow in the
            allocated region. If None, any number of dead boards is permitted,
            as long as the board on the bottom-left corner is alive (Default:
            None).
        max_dead_links : int or None
            The maximum number of broken links allow in the allocated region.
            When require_torus is True this includes wrap-around links,
            otherwise peripheral links are not counted.  If None, any number of
            broken links is allowed. (Default: None).
        require_torus : bool
            If True, only allocate blocks with torus connectivity. In general
            this will only succeed for requests to allocate an entire machine
            (when the machine is otherwise not in use!). Must be False when
            allocating boards. (Default: False)

        Returns
        -------
        bool
        """
        alloc_type = self._alloc_type(*args, **kwargs)
        if alloc_type is _AllocationType.board:
            return self._alloc_board_possible(*args, **kwargs)
        elif alloc_type is _AllocationType.boards:
            return self._alloc_boards_possible(*args, **kwargs)
        else:
            return self._alloc_triads_possible(*args, **kwargs)

    def alloc(self, *args, **kwargs):
        """Attempt to allocate a board or rectangular region of triads of
        boards.

        Usage::

            a.alloc()  # Allocate a single board
            a.alloc(1)  # Allocate a single board
            a.alloc(4)  # Allocate at least 4 boards
            a.alloc(3, 2, 1)  # Allocate a specific board (3, 2, 1)
            a.alloc(2, 3, **kwargs)  # Allocate a 2x3 triad machine

        Parameters
        ----------
        <nothing> OR num OR x, y, z OR width, height
            If all None, allocate a single board.

            If num, allocate at least that number of boards. Special case: if
            1, allocate exactly 1 board.

            If x, y and z, allocate a specific board.

            If width and height, allocate a block of this size, in triads.
        min_ratio : float
            The aspect ratio which the allocated region must be 'at least as
            square as'. Set to 0.0 for any allowable shape. Ignored when
            allocating single boards or specific rectangles of triads.
        max_dead_boards : int or None
            The maximum number of broken or unreachable boards to allow in the
            allocated region. If None, any number of dead boards is permitted,
            as long as the board on the bottom-left corner is alive (Default:
            None).
        max_dead_links : int or None
            The maximum number of broken links allow in the allocated region.
            When require_torus is True this includes wrap-around links,
            otherwise peripheral links are not counted.  If None, any number of
            broken links is allowed. (Default: None).
        require_torus : bool
            If True, only allocate blocks with torus connectivity. In general
            this will only succeed for requests to allocate an entire machine
            (when the machine is otherwise not in use!). Must be False when
            allocating boards. (Default: False)

        Returns
        -------
        (allocation_id, boards, periphery, torus) or None
            If the allocation was successful a four-tuple is returned. If the
            allocation was not successful None is returned.

            The ``allocation_id`` is an integer which should be used to free
            the allocation with the :py:meth:`.free` method. ``boards`` is a
            set of (x, y, z) tuples giving the locations of to allocated
            boards.  ``periphery`` is a set of (x, y, z, link) tuples giving
            the links which leave the allocated set of boards. ``torus`` is a
            :py:class:`.WrapAround` value indicating torus connectivity when at
            least one torus may exist.
        """
        alloc_type = self._alloc_type(*args, **kwargs)
        if alloc_type is _AllocationType.board:
            return self._alloc_board(*args, **kwargs)
        elif alloc_type is _AllocationType.boards:
            return self._alloc_boards(*args, **kwargs)
        else:
            return self._alloc_triads(*args, **kwargs)

    def free(self, allocation_id):
        """Free the resources consumed by the specified allocation.

        Parameters
        ----------
        allocation_id : int
            The ID of the allocation to free.
        """
        type = self.allocation_types.pop(allocation_id)
        x, y, z = self.allocation_board.pop(allocation_id)

        if type is _AllocationType.triads:
            # Simply free the allocation
            self.pack_tree.free(x, y)
        elif type is _AllocationType.board:
            # If the traid the board came from was full, it now isn't...
            if (x, y) in self.full_single_board_triads:
                self.full_single_board_triads.remove((x, y))
                self.single_board_triads[(x, y)] = set()

            # Return the board to the set available in that triad
            self.single_board_triads[(x, y)].add(z)

            # If all working boards have been freed in the triad, we must free
            # the triad.
            working = set(z for z in range(3)
                          if (x, y, z) not in self.dead_boards)
            if self.single_board_triads[(x, y)] == working:
                del self.single_board_triads[(x, y)]
                self.pack_tree.free(x, y)
        else:  # pragma: no cover
            assert False, "Unknown allocation type!"
 def test_already_allocated(self):
     p = PackTree(0, 0, 1, 1)
     assert p.alloc(1, 1) == (0, 0)
     assert p.alloc_area(1) is None