Exemple #1
0
def _swap(vas, vas_location, vbs, vbs_location, l2v, vertices_resources,
          placements, machine):
    """Swap the positions of two sets of vertices.

    Parameters
    ----------
    vas : [vertex, ...]
        A set of vertices currently at vas_location.
    vas_location : (x, y)
    vbs : [vertex, ...]
        A set of vertices currently at vbs_location.
    vbs_location : (x, y)
    l2v : {(x, y): [vertex, ...], ...}
    vertices_resources : {vertex: {resource: value, ...}, ...}
    placements : {vertex: (x, y), ...}
    machine : :py:class:`rig.place_and_route.Machine`
    """
    # Get the lists of vertices at either location
    vas_location2v = l2v[vas_location]
    vbs_location2v = l2v[vbs_location]

    # Get the resource availability at either location
    vas_resources = machine[vas_location]
    vbs_resources = machine[vbs_location]

    # Move all the vertices in vas into vbs.
    for va in vas:
        # Update the placements
        placements[va] = vbs_location

        # Update the location-to-vertex lookup
        vas_location2v.remove(va)
        vbs_location2v.append(va)

        # Update the resource consumption after the move
        resources = vertices_resources[va]
        vas_resources = add_resources(vas_resources, resources)
        vbs_resources = subtract_resources(vbs_resources, resources)

    for vb in vbs:
        # Update the placements
        placements[vb] = vas_location

        # Update the location-to-vertex lookup
        vbs_location2v.remove(vb)
        vas_location2v.append(vb)

        # Update the resource consumption after the move
        resources = vertices_resources[vb]
        vas_resources = subtract_resources(vas_resources, resources)
        vbs_resources = add_resources(vbs_resources, resources)

    # Update the resources in the machine
    machine[vas_location] = vas_resources
    machine[vbs_location] = vbs_resources
Exemple #2
0
def _get_candidate_swap(resources, location, l2v, vertices_resources,
                        fixed_vertices, machine):
    """Given a chip location, select a set of vertices which would have to be
    moved elsewhere to accommodate the arrival of the specified set of
    resources.

    Parameters
    ----------
    resources : {resource: value, ...}
        The amount of resources which are required at the specified location.
    location : (x, y)
        The coordinates of the chip where the resources are sought.
    l2v : {(x, y): [vertex, ...], ...}
    vertices_resources : {vertex: {resource: value, ...}, ...}
    fixed_vertices : {vertex, ...}
    machine : :py:class:`rig.place_and_route.Machine`

    Returns
    -------
    [Vertex, ...] or None
        If a (possibly empty) list, gives the set of vertices which should be
        removed from the specified location to make room.

        If None, the situation is impossible.
    """
    # The resources already available at the given location
    chip_resources = machine[location]

    # The set of vertices at that location
    vertices = l2v[location]

    # The set of vertices to be moved from the location to free up the
    # specified amount of resources
    to_move = []

    # While there's not enough free resource, remove an arbitrary (movable)
    # vertex from the chip.
    i = 0
    while overallocated(subtract_resources(chip_resources, resources)):
        if i >= len(vertices):
            # Run out of vertices to remove from this chip, thus the situation
            # must be impossible.
            return None
        elif vertices[i] in fixed_vertices:
            # Can't move fixed vertices, just skip them.
            i += 1
            continue
        else:
            # Work out the cost change when we remove the specified vertex
            vertex = vertices[i]
            chip_resources = add_resources(chip_resources,
                                           vertices_resources[vertex])
            to_move.append(vertex)
            i += 1

    return to_move
Exemple #3
0
def place(vertices_resources,
          nets,
          machine,
          constraints,
          random=default_random):
    """A random placer.

    This algorithm performs uniform-random placement of vertices (completely
    ignoring connectivty) and thus in the general case is likely to produce
    very poor quality placements. It exists primarily as a baseline comparison
    for placement quality and is probably of little value to most users.

    Parameters
    ----------
    random : :py:class:`random.Random`
        Defaults to ``import random`` but can be set to your own instance of
        :py:class:`random.Random` to allow you to control the seed and produce
        deterministic results. For results to be deterministic,
        vertices_resources must be supplied as an
        :py:class:`collections.OrderedDict`.
    """
    # Within the algorithm we modify the resource availability values in the
    # machine to account for the effects of the current placement. As a result,
    # an internal copy of the structure must be made.
    machine = machine.copy()

    # {vertex: (x, y), ...} gives the location of all vertices, updated
    # throughout the function.
    placements = {}

    # Handle constraints
    vertices_resources, nets, constraints, substitutions = \
        apply_same_chip_constraints(vertices_resources, nets, constraints)
    for constraint in constraints:
        if isinstance(constraint, LocationConstraint):
            # Location constraints are handled by recording the set of fixed
            # vertex locations and subtracting their resources from the chips
            # they're allocated to.
            location = constraint.location
            if location not in machine:
                raise InvalidConstraintError(
                    "Chip requested by {} unavailable".format(machine))
            vertex = constraint.vertex

            # Record the constrained vertex's location
            placements[vertex] = location

            # Make sure the vertex fits at the requested location (updating the
            # resource availability after placement)
            resources = vertices_resources[vertex]
            machine[location] = subtract_resources(machine[location],
                                                   resources)
            if overallocated(machine[location]):
                raise InsufficientResourceError(
                    "Cannot meet {}".format(constraint))
        elif isinstance(
                constraint,  # pragma: no branch
                ReserveResourceConstraint):
            apply_reserve_resource_constraint(machine, constraint)

    # The set of vertices which have not been constrained.
    movable_vertices = [v for v in vertices_resources if v not in placements]

    locations = set(machine)

    for vertex in movable_vertices:
        # Keep choosing random chips until we find one where the vertex fits.
        while True:
            if len(locations) == 0:
                raise InsufficientResourceError(
                    "Ran out of chips while attempting to place vertex "
                    "{}".format(vertex))
            location = random.sample(locations, 1)[0]

            resources_if_placed = subtract_resources(
                machine[location], vertices_resources[vertex])

            if overallocated(resources_if_placed):
                # The vertex won't fit on this chip, we'll assume it is full
                # and not try it in the future.
                locations.remove(location)
            else:
                # The vertex fits: record the resources consumed and move on to
                # the next vertex.
                placements[vertex] = location
                machine[location] = resources_if_placed
                break

    finalise_same_chip_constraints(substitutions, placements)

    return placements
Exemple #4
0
def _step(vertices, d_limit, temperature, placements, l2v, v2n,
          vertices_resources, fixed_vertices, machine, has_wrap_around_links,
          random):
    """Attempt a single swap operation: the kernel of the Simulated Annealing
    algorithm.

    Parameters
    ----------
    vertices : [vertex, ...]
        The set of *movable* vertices.
    d_limit : int
        The maximum distance over-which swaps are allowed.
    temperature : float > 0.0 or None
        The temperature (i.e. likelihood of accepting a non-advantageous swap).
        Higher temperatures mean higher chances of accepting a swap.
    placements : {vertex: (x, y), ...}
        The positions of all vertices, will be updated if a swap is made.
    l2v : {(x, y): [vertex, ...], ...}
        Lookup from chip to vertices, will be updated if a swap is made.
    v2n : {vertex: [:py:class:`rig.netlist.Net`, ...], ...}
        Lookup from vertex to all nets that vertex is in.
    vertices_resources : {vertex: {resource: value, ...}, ...}
    fixed_vertices : {vertex, ...}
        The set of vertices which must not be moved.
    machine : :py:class:`rig.place_and_route.Machine`
        Describes the state of the machine including the resources actually
        available on each chip given the current placements. Updated if a swap
        is made.
    has_wrap_around_links : bool
        Should the placements attempt to make use of wrap-around links?
    random : :py:class:`random.Random`
        The random number generator to use.

    Returns
    -------
    (swapped, delta)
        swapped is a boolean indicating if a swap was made.

        delta is a float indicating the change in cost resulting from the swap
        (or 0.0 when no swap is made).
    """
    # Special case: If the machine is a singleton, no swaps can be made so just
    # terminate.
    if machine.width == 1 and machine.height == 1:
        return (False, 0.0)

    # Select a vertex to swap at random
    src_vertex = random.choice(vertices)

    # Select a random (nearby) location to swap the vertex with. Note: this is
    # guaranteed to be different from the selected vertex, otherwise the swap
    # cannot change the cost of the placements.
    # XXX: Does not consider hexagonal properties of the system!
    src_location = placements[src_vertex]
    dst_location = src_location
    while dst_location == src_location:
        if has_wrap_around_links:
            dst_location = tuple(
                random.randint(v - d_limit, v + d_limit) % limit
                for v, limit in [(
                    src_location[0],
                    machine.width), (src_location[1], machine.height)])
        else:
            dst_location = tuple(
                random.randint(max(v -
                                   d_limit, 0), min(v + d_limit, limit - 1))
                for v, limit in [(
                    src_location[0],
                    machine.width), (src_location[1], machine.height)])

    # If we've inadvertently selected a dead chip to swap to, abort the swap.
    if dst_location not in machine:
        return (False, 0.0)

    # Find out which vertices (if any) must be swapped out of the destination
    # to make room for the vertex we're moving.
    src_resources = vertices_resources[src_vertex]
    dst_vertices = _get_candidate_swap(src_resources, dst_location, l2v,
                                       vertices_resources, fixed_vertices,
                                       machine)

    # The destination simply isn't big enough (no matter how many vertices at
    # the destination are moved), abort the swap.
    if dst_vertices is None:
        return (False, 0.0)

    # Make sure that any vertices moved out of the destination will fit in the
    # space left in the source location. If there isn't enough space, abort the
    # swap.
    resources = machine[src_location]
    resources = add_resources(resources, src_resources)
    for dst_vertex in dst_vertices:
        resources = subtract_resources(resources,
                                       vertices_resources[dst_vertex])
    if overallocated(resources):
        return (False, 0.0)

    # Work out the cost of the nets involved *before* swapping
    cost_before = _vertex_net_cost(src_vertex, v2n, placements,
                                   has_wrap_around_links, machine)
    for dst_vertex in dst_vertices:
        cost_before += _vertex_net_cost(dst_vertex, v2n, placements,
                                        has_wrap_around_links, machine)

    # Swap the vertices
    _swap([src_vertex], src_location, dst_vertices, dst_location, l2v,
          vertices_resources, placements, machine)

    # Work out the new cost
    cost_after = _vertex_net_cost(src_vertex, v2n, placements,
                                  has_wrap_around_links, machine)
    for dst_vertex in dst_vertices:
        cost_after += _vertex_net_cost(dst_vertex, v2n, placements,
                                       has_wrap_around_links, machine)

    # If the swap was beneficial, keep it, otherwise keep it with a probability
    # related to just how bad the cost change is is and the temperature.
    delta = cost_after - cost_before
    if delta <= 0.0 or random.random() < math.exp(-delta / temperature):
        # Keep the swap!
        return (True, delta)
    else:
        # Revert the swap
        _swap([src_vertex], dst_location, dst_vertices, src_location, l2v,
              vertices_resources, placements, machine)
        return (False, 0.0)
def _initial_placement(movable_vertices, vertices_resources, machine, random):
    """For internal use. Produces a random, sequential initial placement,
    updating the resource availabilities of every core in the supplied machine.

    Parameters
    ----------
    movable_vertices : {vertex, ...}
        A set of the vertices to be given a random initial placement.
    vertices_resources : {vertex: {resource: value, ...}, ...}
    machine : :py:class:`rig.place_and_route.Machine`
        A machine object describing the machine into which the vertices should
        be placed.

        All chips hosting fixed vertices should have a chip_resource_exceptions
        entry which accounts for the allocated resources.

        When this function returns, the machine.chip_resource_exceptions will
        be updated to account for the resources consumed by the initial
        placement of movable vertices.
    random : :py:class`random.Random`
        The random number generator to use

    Returns
    -------
    {vertex: (x, y), ...}
        For all movable_vertices.

    Raises
    ------
    InsufficientResourceError
    InvalidConstraintError
    """
    # Initially fill chips in the system in a random order
    locations = list(machine)
    random.shuffle(locations)
    location_iter = iter(locations)

    # Greedily place the vertices in a random order
    movable_vertices = list(movable_vertices)
    random.shuffle(movable_vertices)
    vertex_iter = iter(movable_vertices)

    placement = {}
    try:
        location = next(location_iter)
    except StopIteration:
        raise InsufficientResourceError("No working chips in system.")
    while True:
        # Get a vertex to place
        try:
            vertex = next(vertex_iter)
        except StopIteration:
            # All vertices have been placed
            break

        # Advance through the set of available locations until we find a chip
        # where the vertex fits
        while True:
            resources_if_placed = subtract_resources(
                machine[location], vertices_resources[vertex])

            if overallocated(resources_if_placed):
                # The vertex won't fit on this chip, move onto the next chip
                try:
                    location = next(location_iter)
                    continue
                except StopIteration:
                    raise InsufficientResourceError(
                        "Ran out of chips while attempting to place vertex "
                        "{}".format(vertex))
            else:
                # The vertex fits: record the resources consumed and move on to
                # the next vertex.
                placement[vertex] = location
                machine[location] = resources_if_placed
                break

    return placement
def place(vertices_resources,
          nets,
          machine,
          constraints,
          effort=1.0,
          random=default_random,
          on_temperature_change=None,
          kernel=default_kernel,
          kernel_kwargs={}):
    """A flat Simulated Annealing based placement algorithm.

    This placement algorithm uses simulated annealing directly on the supplied
    problem graph with the objective of reducing wire lengths (and thus,
    indirectly, the potential for congestion). Though computationally
    expensive, this placer produces relatively good placement solutions.

    The annealing temperature schedule used by this algorithm is taken from
    "VPR: A New Packing, Placement and Routing Tool for FPGA Research" by
    Vaughn Betz and Jonathan Rose from the "1997 International Workshop on
    Field Programmable Logic and Applications".

    Two implementations of the algorithm's kernel are available:

    * :py:class:`~rig.place_and_route.place.sa.python_kernel.PythonKernel` A
      pure Python implementation which is available on all platforms supported
      by Rig.
    * :py:class:`~rig.place_and_route.place.sa.c_kernel.CKernel` A C
      implementation which is typically 50-150x faster than the basic Python
      kernel. Since this implementation requires a C compiler during
      installation, it is an optional feature of Rig. See the
      :py:class:`CKernel's documentation
      <rig.place_and_route.place.sa.c_kernel.CKernel>` for details.

    The fastest kernel installed is used by default and can be manually chosen
    using the ``kernel`` argument.

    This algorithm produces INFO level logging information describing the
    progress made by the algorithm.

    .. warning:
        This algorithm does not attempt to produce good solutions to the
        bin-packing problem of optimally fitting vertices into chips and it may
        fail if a good placement requires good bin packing.

    Parameters
    ----------
    effort : float
        A scaling factor for the number of iterations the algorithm should run
        for. 1.0 is probably about as low as you'll want to go in practice and
        runtime increases linearly as you increase this parameter.
    random : :py:class:`random.Random`
        A Python random number generator. Defaults to ``import random`` but can
        be set to your own instance of :py:class:`random.Random` to allow you
        to control the seed and produce deterministic results. For results to
        be deterministic, vertices_resources must be supplied as an
        :py:class:`collections.OrderedDict`.
    on_temperature_change : callback_function or None
        An (optional) callback function which is called every time the
        temperature is changed. This callback can be used to provide status
        updates

        The callback function is passed the following arguments:

        * ``iteration_count``: the number of iterations the placer has
          attempted (integer)
        * ``placements``: The current placement solution.
        * ``cost``: the weighted sum over all nets of bounding-box size.
          (float)
        * ``acceptance_rate``: the proportion of iterations which have resulted
          in an accepted change since the last callback call. (float between
          0.0 and 1.0)
        * ``temperature``: The current annealing temperature. (float)
        * ``distance_limit``: The maximum distance any swap may be made over.
          (integer)

        If the callback returns False, the anneal is terminated immediately and
        the current solution is returned.
    kernel : :py:class:`~rig.place_and_route.place.sa.kernel.Kernel`
        A simulated annealing placement kernel. A sensible default will be
        chosen based on the available kernels on this machine. The kernel may
        not be used if the placement problem has a trivial solution.
    kernel_kwargs : dict
        Optional kernel-specific keyword arguments to pass to the kernel
        constructor.
    """
    # Special case: just return immediately when there's nothing to place
    if len(vertices_resources) == 0:
        return {}

    # Within the algorithm we modify the resource availability values in the
    # machine to account for the effects of the current placement. As a result,
    # an internal copy of the structure must be made.
    machine = machine.copy()

    # {vertex: (x, y), ...} gives the location of all vertices whose position
    # is fixed by a LocationConstraint.
    fixed_vertices = {}

    # Handle constraints
    vertices_resources, nets, constraints, substitutions = \
        apply_same_chip_constraints(vertices_resources, nets, constraints)
    for constraint in constraints:
        if isinstance(constraint, LocationConstraint):
            # Location constraints are handled by recording the set of fixed
            # vertex locations and subtracting their resources from the chips
            # they're allocated to. These vertices will then not be added to
            # the internal placement data structure to prevent annealing from
            # moving them. They will be re-introduced at the last possible
            # moment.
            location = constraint.location
            if location not in machine:
                raise InvalidConstraintError(
                    "Chip requested by {} unavailable".format(machine))
            vertex = constraint.vertex

            # Record the constrained vertex's location
            fixed_vertices[vertex] = location

            # Make sure the vertex fits at the requested location (updating the
            # resource availability after placement)
            resources = vertices_resources[vertex]
            machine[location] = subtract_resources(machine[location],
                                                   resources)
            if overallocated(machine[location]):
                raise InsufficientResourceError(
                    "Cannot meet {}".format(constraint))
        elif isinstance(
                constraint,  # pragma: no branch
                ReserveResourceConstraint):
            apply_reserve_resource_constraint(machine, constraint)

    # Initially randomly place the movable vertices
    movable_vertices = {
        v
        for v in vertices_resources if v not in fixed_vertices
    }
    initial_placements = _initial_placement(movable_vertices,
                                            vertices_resources, machine,
                                            random)

    # Include the fixed vertices in initial placement
    initial_placements.update(fixed_vertices)

    # Filter out empty or singleton nets and those weighted as zero since they
    # cannot influence placement.
    nets = [n for n in nets if len(set(n)) > 1 and n.weight > 0.0]

    # Special cases where no placement effort is required:
    # * There is only one chip
    # * There are no resource types to be consumed
    # * No effort is to be made
    # * No movable vertices
    # * There are no nets (and moving things has no effect)
    trivial = ((machine.width, machine.height) == (1, 1)
               or len(machine.chip_resources) == 0 or effort == 0.0
               or len(movable_vertices) == 0 or len(nets) == 0)
    if trivial:
        logger.info("Placement has trivial solution. SA not used.")
        finalise_same_chip_constraints(substitutions, initial_placements)
        return initial_placements

    # Intialise the algorithm kernel
    k = kernel(vertices_resources, movable_vertices, set(fixed_vertices),
               initial_placements, nets, machine, random, **kernel_kwargs)

    logger.info("SA placement kernel: %s", kernel.__name__)

    # Specifies the maximum distance any swap can span. Initially consider
    # swaps that span the entire machine.
    distance_limit = max(machine.width, machine.height)

    # Determine initial temperature according to the heuristic used by VPR: 20
    # times the standard deviation of len(movable_vertices) random swap costs.
    # The arbitrary very-high temperature is used to cause "all" swaps to be
    # accepted.
    _0, _1, cost_delta_sd = k.run_steps(len(movable_vertices), distance_limit,
                                        1e100)
    temperature = 20.0 * cost_delta_sd

    # The number of swap-attempts between temperature changes is selected by
    # the heuristic used by VPR. This value is scaled linearly by the effort
    # parameter.
    num_steps = max(1, int(effort * len(vertices_resources)**1.33))

    logger.info("Initial placement temperature: %0.1f", temperature)

    # Counter for the number of swap attempts made (used for diagnostic
    # purposes)
    iteration_count = 0

    # Holds the total cost of the current placement. This default value chosen
    # to ensure the loop below iterates at least once.
    current_cost = 0.0

    # The annealing algorithm runs until a heuristic termination condition
    # (taken from VPR) is hit. The heuristic waits until the temperature falls
    # below a small fraction of the average net cost.
    while temperature > (0.005 * current_cost) / len(nets):
        # Run an iteration at the current temperature
        num_accepted, current_cost, _ = k.run_steps(
            num_steps, int(math.ceil(distance_limit)), temperature)

        # The ratio of accepted-to-not-accepted changes
        r_accept = num_accepted / float(num_steps)

        # Special case: Can't do better than 0 cost! This is a special case
        # since the normal termination condition will not terminate if the cost
        # doesn't drop below 0.
        if current_cost == 0:
            break

        # The temperature is reduced by a factor heuristically based on the
        # acceptance rate. The schedule below attempts to maximise the time
        # spent at temperatures where a large portion (but not all) of changes
        # are being accepted. If lots of changes are being accepted (e.g.
        # during high-temperature periods) then most of them are likely not to
        # be beneficial. If few changes are being accepted, we're probably
        # pretty close to the optimal placement.
        if r_accept > 0.96:
            alpha = 0.5
        elif r_accept > 0.8:
            alpha = 0.9
        elif r_accept > 0.15:
            alpha = 0.95
        else:
            alpha = 0.8
        temperature = alpha * temperature

        # According to:
        # * M. Huang, F. Romeo, and A. Sangiovanni-Vincentelli, "An Efficient
        #   General Cooling Schedule for Simulated Annealing" ICCAD, 1986, pp.
        #   381 - 384 and J. Lam
        # * J. Delosme, "Performance of a New Annealing Schedule" DAC, 1988,
        #   pp. 306 - 311.
        # It is desirable to keep the acceptance ratio as close to 0.44 for as
        # long as possible. As a result, when r_accept falls below this we can
        # help increase the acceptance rate by reducing the set of possible
        # swap candidates based on the observation that near the end of
        # placement, most things are near their optimal location and thus long
        # distance swaps are unlikely to be useful.
        distance_limit *= 1.0 - 0.44 + r_accept
        distance_limit = min(max(distance_limit, 1.0),
                             max(machine.width, machine.height))

        iteration_count += num_steps
        logger.debug(
            "Iteration: %d, "
            "Cost: %0.1f, "
            "Kept: %0.1f%%, "
            "Temp: %0.3f, "
            "Dist: %d.", iteration_count, current_cost, r_accept * 100,
            temperature, math.ceil(distance_limit))

        # Call the user callback before the next iteration, terminating if
        # requested.
        if on_temperature_change is not None:
            placements = k.get_placements().copy()
            finalise_same_chip_constraints(substitutions, placements)
            ret_val = on_temperature_change(iteration_count, placements,
                                            current_cost, r_accept,
                                            temperature, distance_limit)
            if ret_val is False:
                break

    logger.info("Anneal terminated after %d iterations.", iteration_count)

    placements = k.get_placements()
    finalise_same_chip_constraints(substitutions, placements)

    return placements
def place(vertices_resources,
          nets,
          machine,
          constraints,
          vertex_order=None,
          chip_order=None):
    """Blindly places vertices in sequential order onto chips in the machine.

    This algorithm sequentially places vertices onto chips in the order
    specified (or in an undefined order if not specified). This algorithm is
    essentially the simplest possible valid placement algorithm and is intended
    to form the basis of other simple sequential and greedy placers.

    The algorithm proceeds by attempting to place each vertex on the a chip. If
    the vertex fits we move onto the next vertex (but keep filling the same
    vertex). If the vertex does not fit we move onto the next candidate chip
    until we find somewhere the vertex fits. The algorithm will raise an
    :py:exc:`rig.place_and_route.exceptions.InsufficientResourceError`
    if it has failed to fit a vertex on every chip.

    Parameters
    ----------
    vertex_order : None or iterable
        The order in which the vertices should be attemted to be placed.

        If None (the default), the vertices will be placed in the default
        iteration order of the ``vertices_resources`` argument. If an iterable,
        the iteration sequence should produce each vertex in vertices_resources
        *exactly once*.

    chip_order : None or iterable
        The order in which chips should be tried as a candidate location for a
        vertex.

        If None (the default), the chips will be used in the default iteration
        order of the ``machine`` object (a raster scan). If an iterable, the
        iteration sequence should produce (x, y) pairs giving the coordinates
        of chips to use. All working chip coordinates must be included in the
        iteration sequence *exactly once*. Additional chip coordinates of
        non-existant or dead chips are also allowed (and will simply be
        skipped).
    """
    # If no vertices to place, just stop (from here on we presume that at least
    # one vertex will be placed)
    if len(vertices_resources) == 0:
        return {}

    # Within the algorithm we modify the resource availability values in the
    # machine to account for the effects of the current placement. As a result,
    # an internal copy of the structure must be made.
    machine = machine.copy()

    # {vertex: (x, y), ...} gives the location of all vertices, updated
    # throughout the function.
    placements = {}

    # Handle constraints
    vertices_resources, nets, constraints, substitutions = \
        apply_same_chip_constraints(vertices_resources, nets, constraints)
    for constraint in constraints:
        if isinstance(constraint, LocationConstraint):
            # Location constraints are handled by recording the set of fixed
            # vertex locations and subtracting their resources from the chips
            # they're allocated to.
            location = constraint.location
            if location not in machine:
                raise InvalidConstraintError(
                    "Chip requested by {} unavailable".format(machine))
            vertex = constraint.vertex

            # Record the constrained vertex's location
            placements[vertex] = location

            # Make sure the vertex fits at the requested location (updating the
            # resource availability after placement)
            resources = vertices_resources[vertex]
            machine[location] = subtract_resources(machine[location],
                                                   resources)
            if overallocated(machine[location]):
                raise InsufficientResourceError(
                    "Cannot meet {}".format(constraint))
        elif isinstance(
                constraint,  # pragma: no branch
                ReserveResourceConstraint):
            apply_reserve_resource_constraint(machine, constraint)

    if vertex_order is not None:
        # Must modify the vertex_order to substitute the merged vertices
        # inserted by apply_reserve_resource_constraint.
        vertex_order = list(vertex_order)
        for merged_vertex in reversed(substitutions):
            # Swap the first merged vertex for its MergedVertex object and
            # remove all other vertices from the merged set
            vertex_order[vertex_order.index(merged_vertex.vertices[0])] \
                = merged_vertex
            # Remove all other vertices in the MergedVertex
            for vertex in merged_vertex.vertices[1:]:
                vertex_order.remove(vertex)

    # The set of vertices which have not been constrained, in iteration order
    movable_vertices = (v for v in (
        vertices_resources if vertex_order is None else vertex_order)
                        if v not in placements)

    # A cyclic iterator over all available chips
    chips = cycle(c for c in (machine if chip_order is None else chip_order)
                  if c in machine)
    chips_iter = iter(chips)

    try:
        cur_chip = next(chips_iter)
    except StopIteration:
        raise InsufficientResourceError("No working chips in machine.")

    # The last chip that we successfully placed something on. Used to detect
    # when we've tried all available chips and not found a suitable candidate
    last_successful_chip = cur_chip

    # Place each vertex in turn
    for vertex in movable_vertices:
        while True:
            resources_if_placed = subtract_resources(
                machine[cur_chip], vertices_resources[vertex])

            if not overallocated(resources_if_placed):
                # The vertex fits: record the resources consumed and move on to
                # the next vertex.
                placements[vertex] = cur_chip
                machine[cur_chip] = resources_if_placed
                last_successful_chip = cur_chip
                break
            else:
                # The vertex won't fit on this chip, move onto the next one
                # available.
                cur_chip = next(chips_iter)

                # If we've looped around all the available chips without
                # managing to place the vertex, give up!
                if cur_chip == last_successful_chip:
                    raise InsufficientResourceError(
                        "Ran out of chips while attempting to place vertex "
                        "{}".format(vertex))

    finalise_same_chip_constraints(substitutions, placements)

    return placements