예제 #1
0
파일: exact.py 프로젝트: GRSEB9S/region
def _order(adj, attr, n_regions, solver, metric):
    """
    Parameters
    ----------
    adj : class:`scipy.sparse.csr_matrix`
        Refer to the corresponding argument in :func:`_flow`.
    attr : :class:`numpy.ndarray`
        Refer to the corresponding argument in :func:`_flow`.
    n_regions : int
        Refer to the corresponding argument in :func:`_flow`.
    solver : str
        Refer to the corresponding argument in :func:`_flow`.
    metric : function
        Refer to the corresponding argument in :func:`_flow`.

    Returns
    -------
    result : :class:`numpy.ndarray`
        Refer to the return value in :func:`_flow`.
    """
    print("running ORDER algorithm")  # TODO: rm
    prob = LpProblem("Order", LpMinimize)

    # Parameters of the optimization problem
    n_areas = attr.shape[0]
    I = list(range(n_areas))  # index for areas
    II = [(i, j) for i in I for j in I]
    II_upper_triangle = [(i, j) for i, j in II if i < j]
    K = range(n_regions)  # index for regions
    O = range(n_areas - n_regions)  # index for orders
    d = {
        (i, j): metric(
            attr[i].reshape(attr.shape[1], 1),  # reshaping to...
            attr[j].reshape(attr.shape[1], 1))  # ...avoid warnings
        for i, j in II_upper_triangle
    }

    # Decision variables
    t = LpVariable.dicts("t", ((i, j) for i, j in II_upper_triangle),
                         lowBound=0,
                         upBound=1,
                         cat=LpInteger)
    x = LpVariable.dicts("x", ((i, k, o) for i in I for k in K for o in O),
                         lowBound=0,
                         upBound=1,
                         cat=LpInteger)

    # Objective function
    # (13) in Duque et al. (2011): "The p-Regions Problem"
    prob += lpSum(d[i, j] * t[i, j] for i, j in II_upper_triangle)

    # Constraints
    # (14) in Duque et al. (2011): "The p-Regions Problem"
    for k in K:
        prob += sum(x[i, k, 0] for i in I) == 1
    # (15) in Duque et al. (2011): "The p-Regions Problem"
    for i in I:
        prob += sum(x[i, k, o] for k in K for o in O) == 1
    # (16) in Duque et al. (2011): "The p-Regions Problem"
    for i in I:
        for k in K:
            for o in range(1, len(O)):
                prob += x[i, k, o] <= \
                        sum(x[j, k, o-1] for j in neighbors(adj, i))
    # (17) in Duque et al. (2011): "The p-Regions Problem"
    for i, j in II_upper_triangle:
        for k in K:
            summ = sum(x[i, k, o] + x[j, k, o] for o in O) - 1
            prob += t[i, j] >= summ
    # (18) in Duque et al. (2011): "The p-Regions Problem"
    # already in LpVariable-definition
    # (19) in Duque et al. (2011): "The p-Regions Problem"
    # already in LpVariable-definition

    # Solve the optimization problem
    solver = get_solver_instance(solver)
    prob.solve(solver)
    result = np.zeros(n_areas)
    for i in I:
        for k in K:
            for o in O:
                if x[i, k, o].varValue == 1:
                    result[i] = k
    return result
예제 #2
0
파일: exact.py 프로젝트: GRSEB9S/region
def _tree(adj, attr, n_regions, solver, metric):
    """
    Parameters
    ----------
    adj : class:`scipy.sparse.csr_matrix`
        Refer to the corresponding argument in :func:`_flow`.
    attr : :class:`numpy.ndarray`
        Refer to the corresponding argument in :func:`_flow`.
    n_regions : int
        Refer to the corresponding argument in :func:`_flow`.
    solver : str
        Refer to the corresponding argument in :func:`_flow`.
    metric : function
        Refer to the corresponding argument in :func:`_flow`.

    Returns
    -------
    result : :class:`numpy.ndarray`
        Refer to the return value in :func:`_flow`.
    """
    print("running TREE algorithm")  # TODO: rm
    prob = LpProblem("Tree", LpMinimize)

    # Parameters of the optimization problem
    n_areas = attr.shape[0]
    I = list(range(n_areas))  # index for areas
    II = [(i, j) for i in I for j in I]
    II_upper_triangle = [(i, j) for i, j in II if i < j]
    d = {
        (i, j): metric(
            attr[i].reshape(attr.shape[1], 1),  # reshaping to...
            attr[j].reshape(attr.shape[1], 1))  # ...avoid warnings
        for i, j in II
    }
    # Decision variables
    t = LpVariable.dicts("t", ((i, j) for i, j in II),
                         lowBound=0,
                         upBound=1,
                         cat=LpInteger)
    x = LpVariable.dicts("x", ((i, j) for i, j in II),
                         lowBound=0,
                         upBound=1,
                         cat=LpInteger)
    u = LpVariable.dicts("u", (i for i in I), lowBound=0, cat=LpInteger)

    # Objective function
    # (3) in Duque et al. (2011): "The p-Regions Problem"
    prob += lpSum(d[i, j] * t[i, j] for i, j in II_upper_triangle)

    # Constraints
    # (4) in Duque et al. (2011): "The p-Regions Problem"
    lhs = lpSum(x[i, j] for i in I for j in neighbors(adj, i))
    prob += lhs == n_areas - n_regions
    # (5) in Duque et al. (2011): "The p-Regions Problem"
    for i in I:
        prob += lpSum(x[i, j] for j in neighbors(adj, i)) <= 1
    # (6) in Duque et al. (2011): "The p-Regions Problem"
    for i in I:
        for j in I:
            for m in I:
                if i != j and i != m and j != m:
                    prob += t[i, j] + t[i, m] - t[j, m] <= 1
    # (7) in Duque et al. (2011): "The p-Regions Problem"
    for i, j in II:
        prob += t[i, j] - t[j, i] == 0
    # (8) in Duque et al. (2011): "The p-Regions Problem"
    for i in I:
        for j in neighbors(adj, i):
            prob += x[i, j] <= t[i, j]
    # (9) in Duque et al. (2011): "The p-Regions Problem"
    for i in I:
        for j in neighbors(adj, i):
            prob += u[i] - u[j] + (n_areas - n_regions) * x[i, j] \
                    + (n_areas - n_regions - 2) * x[j, i] \
                    <= n_areas - n_regions - 1
    # (10) in Duque et al. (2011): "The p-Regions Problem"
    for i in I:
        prob += u[i] <= n_areas - n_regions
        prob += u[i] >= 1
    # (11) in Duque et al. (2011): "The p-Regions Problem"
    # already in LpVariable-definition
    # (12) in Duque et al. (2011): "The p-Regions Problem"
    # already in LpVariable-definition

    # Solve the optimization problem
    solver = get_solver_instance(solver)
    prob.solve(solver)

    # build a list of regions like [[0, 1, 2, 5], [3, 4, 6, 7, 8]]
    idx_copy = set(I)
    regions = [[] for _ in range(n_regions)]
    for i in range(n_regions):
        area = idx_copy.pop()
        regions[i].append(area)

        for other_area in idx_copy:
            if t[area, other_area].varValue == 1:
                regions[i].append(other_area)

        idx_copy.difference_update(regions[i])

    result = array_from_region_list(regions)
    return result
예제 #3
0
파일: exact.py 프로젝트: GRSEB9S/region
def _flow(adj, attr, n_regions, solver, metric):
    """
    Parameters
    ----------
    adj : class:`scipy.sparse.csr_matrix`
        See the corresponding argument in
        :meth:`PRegionsExact.fit_from_scipy_sparse_matrix`.
    attr : :class:`numpy.ndarray`
        See the corresponding argument in
        :meth:`PRegionsExact.fit_from_scipy_sparse_matrix`.
    n_regions : int
        See the corresponding argument in
        :meth:`PRegionsExact.fit_from_scipy_sparse_matrix`.
    solver : str
        See the corresponding argument in
        :meth:`PRegionsExact.fit_from_scipy_sparse_matrix`.
    metric : function
        A function fulfilling the 4 conditions described in the docsting of
        :func:`region.util.get_metric_function`.

    Returns
    -------
    result : :class:`numpy.ndarray`
        A one-dimensional array containing each area's region label.
    """
    print("running FLOW algorithm")  # TODO: rm
    prob = LpProblem("Flow", LpMinimize)

    # Parameters of the optimization problem
    n_areas = adj.shape[0]
    I = list(range(n_areas))  # index for areas
    II = [(i, j) for i in I for j in I]
    II_upper_triangle = [(i, j) for i, j in II if i < j]
    K = range(n_regions)  # index for regions
    d = {
        (i, j): metric(
            attr[i].reshape(attr.shape[1], 1),  # reshaping to...
            attr[j].reshape(attr.shape[1], 1))  # ...avoid warnings
        for i, j in II_upper_triangle
    }

    # Decision variables
    t = LpVariable.dicts("t", ((i, j) for i, j in II_upper_triangle),
                         lowBound=0,
                         upBound=1,
                         cat=LpInteger)
    f = LpVariable.dicts(  # The amount of flow (non-negative integer)
        "f",  # from area i to j in region k.
        ((i, j, k) for i in I for j in neighbors(adj, i) for k in K),
        lowBound=0,
        cat=LpInteger)
    y = LpVariable.dicts(  # 1 if area i is assigned to region k. 0 otherwise.
        "y", ((i, k) for i in I for k in K),
        lowBound=0,
        upBound=1,
        cat=LpInteger)
    w = LpVariable.dicts(  # 1 if area i is chosen as a sink. 0 otherwise.
        "w", ((i, k) for i in I for k in K),
        lowBound=0,
        upBound=1,
        cat=LpInteger)

    # Objective function
    # (20) in Duque et al. (2011): "The p-Regions Problem"
    prob += lpSum(d[i, j] * t[i, j] for i, j in II_upper_triangle)

    # Constraints
    # (21) in Duque et al. (2011): "The p-Regions Problem"
    for i in I:
        prob += sum(y[i, k] for k in K) == 1
    # (22) in Duque et al. (2011): "The p-Regions Problem"
    for i in I:
        for k in K:
            prob += w[i, k] <= y[i, k]
    # (23) in Duque et al. (2011): "The p-Regions Problem"
    for k in K:
        prob += sum(w[i, k] for i in I) == 1
    # (24) in Duque et al. (2011): "The p-Regions Problem"
    for i in I:
        for j in neighbors(adj, i):
            for k in K:
                prob += f[i, j, k] <= y[i, k] * (n_areas - n_regions)
    # (25) in Duque et al. (2011): "The p-Regions Problem"
    for i in I:
        for j in neighbors(adj, i):
            for k in K:
                prob += f[i, j, k] <= y[j, k] * (n_areas - n_regions)
    # (26) in Duque et al. (2011): "The p-Regions Problem"
    for i in I:
        for k in K:
            lhs = sum(f[i, j, k] - f[j, i, k] for j in neighbors(adj, i))
            prob += lhs >= y[i, k] - (n_areas - n_regions) * w[i, k]
    # (27) in Duque et al. (2011): "The p-Regions Problem"
    for i, j in II_upper_triangle:
        for k in K:
            prob += t[i, j] >= y[i, k] + y[j, k] - 1
    # (28) in Duque et al. (2011): "The p-Regions Problem"
    # already in LpVariable-definition
    # (29) in Duque et al. (2011): "The p-Regions Problem"
    # already in LpVariable-definition
    # (30) in Duque et al. (2011): "The p-Regions Problem"
    # already in LpVariable-definition
    # (31) in Duque et al. (2011): "The p-Regions Problem"
    # already in LpVariable-definition

    # Solve the optimization problem
    solver = get_solver_instance(solver)
    prob.solve(solver)
    result = np.zeros(n_areas)
    for i in I:
        for k in K:
            if y[i, k].varValue == 1:
                result[i] = k
    return result
예제 #4
0
    def fit_from_scipy_sparse_matrix(self,
                                     adj,
                                     attr,
                                     spatially_extensive_attr,
                                     threshold,
                                     solver="cbc",
                                     metric="euclidean"):
        """
        Solve the max-p-regions problem as MIP as described in [DAR2012]_.

        The resulting region labels are assigned to the instance's
        :attr:`labels_` attribute.

        Parameters
        ----------
        adj : class:`scipy.sparse.csr_matrix`
            Adjacency matrix representing the areas' contiguity relation.
        attr : :class:`numpy.ndarray`
            Array (number of areas x number of attributes) of areas' attributes
            relevant to clustering.
        spatially_extensive_attr : :class:`numpy.ndarray`
            Array (number of areas x number of attributes) of areas' attributes
            relevant to ensuring the threshold condition.
        threshold : numbers.Real or :class:`numpy.ndarray`
            The lower bound for a region's sum of spatially extensive
            attributes. The argument's type is numbers.Real if there is only
            one spatially extensive attribute per area, otherwise it is a
            one-dimensional array with as many entries as there are spatially
            extensive attributes per area.
        solver : {"cbc", "cplex", "glpk", "gurobi"}, default: "cbc"
            The solver to use. Unless the default solver is used, the user has
            to make sure that the specified solver is installed.

            * "cbc" - the Cbc (Coin-or branch and cut) solver
            * "cplex" - the CPLEX solver
            * "glpk" - the GLPK (GNU Linear Programming Kit) solver
            * "gurobi" - the Gurobi Optimizer

        metric : str or function, default: "euclidean"
            See the `metric` argument in
            :func:`region.util.get_metric_function`.
        """
        self.metric = get_metric_function(metric)
        check_solver(solver)

        prob = LpProblem("Max-p-Regions", LpMinimize)

        # Parameters of the optimization problem
        n_areas = adj.shape[0]
        I = list(range(n_areas))  # index for areas
        II = [(i, j) for i in I for j in I]
        II_upper_triangle = [(i, j) for i, j in II if i < j]
        # index of potential regions, called k in [DAR2012]_:
        K = range(n_areas)
        # index of contiguity order, called c in [DAR2012]_:
        O = range(n_areas)
        d = {(i, j): self.metric(attr[i].reshape(1, -1),
                                 attr[j].reshape(1, -1))
             for i, j in II_upper_triangle}
        h = 1 + floor(log10(sum(d[(i, j)] for i, j in II_upper_triangle)))

        # Decision variables
        t = LpVariable.dicts("t", ((i, j) for i, j in II_upper_triangle),
                             lowBound=0,
                             upBound=1,
                             cat=LpInteger)
        x = LpVariable.dicts("x", ((i, k, o) for i in I for k in K for o in O),
                             lowBound=0,
                             upBound=1,
                             cat=LpInteger)

        # Objective function
        # (1) in Duque et al. (2012): "The Max-p-Regions Problem"
        prob += -10**h * lpSum(x[i, k, 0] for k in K for i in I) \
            + lpSum(d[i, j] * t[i, j] for i, j in II_upper_triangle)

        # Constraints
        # (2) in Duque et al. (2012): "The Max-p-Regions Problem"
        for k in K:
            prob += lpSum(x[i, k, 0] for i in I) <= 1
        # (3) in Duque et al. (2012): "The Max-p-Regions Problem"
        for i in I:
            prob += lpSum(x[i, k, o] for k in K for o in O) == 1
        # (4) in Duque et al. (2012): "The Max-p-Regions Problem"
        for i in I:
            for k in K:
                for o in range(1, len(O)):
                    prob += x[i, k, o] <= lpSum(x[j, k, o - 1]
                                                for j in neighbors(adj, i))
        # (5) in Duque et al. (2012): "The Max-p-Regions Problem"
        if isinstance(spatially_extensive_attr[I[0]], numbers.Real):
            for k in K:
                lhs = lpSum(x[i, k, o] * spatially_extensive_attr[i] for i in I
                            for o in O)
                prob += lhs >= threshold * lpSum(x[i, k, 0] for i in I)
        elif isinstance(spatially_extensive_attr[I[0]], collections.Iterable):
            for el in range(len(spatially_extensive_attr[I[0]])):
                for k in K:
                    lhs = lpSum(x[i, k, o] * spatially_extensive_attr[i][el]
                                for i in I for o in O)
                    if isinstance(threshold, numbers.Real):
                        rhs = threshold * lpSum(x[i, k, 0] for i in I)
                        prob += lhs >= rhs
                    elif isinstance(threshold, np.ndarray):
                        rhs = threshold[el] * lpSum(x[i, k, 0] for i in I)
                        prob += lhs >= rhs
        # (6) in Duque et al. (2012): "The Max-p-Regions Problem"
        for i, j in II_upper_triangle:
            for k in K:
                prob += t[i, j] >= \
                        lpSum(x[i, k, o] + x[j, k, o] for o in O) - 1
        # (7) in Duque et al. (2012): "The Max-p-Regions Problem"
        # already in LpVariable-definition
        # (8) in Duque et al. (2012): "The Max-p-Regions Problem"
        # already in LpVariable-definition

        # additional constraint for speedup (p. 405 in [DAR2012]_)
        for o in O:
            prob += x[I[0], K[0], o] == (1 if o == 0 else 0)

        # Solve the optimization problem
        solver = get_solver_instance(solver)
        print("start solving with", solver)
        prob.solve(solver)
        print("solved")
        result = np.zeros(n_areas)
        for i in I:
            for k in K:
                for o in O:
                    if x[i, k, o].varValue == 1:
                        result[i] = k
        self.labels_ = result
        self.solver = solver
예제 #5
0
    def _azp_connected_component(self, adj, initial_labels, attr):
        """
        Implementation of the reactive tabu version of the AZP algorithm (refer
        to [OR1995]_) for a spatially connected set of areas (i.e. for every
        area there is a path to every other area).

        Parameters
        ----------
        adj : :class:`scipy.sparse.csr_matrix`
            Refer to the corresponding argument in
            :meth:`AZP._azp_connected_component`.
        initial_labels : :class:`numpy.ndarray`
            Refer to the corresponding argument in
            :meth:`AZP._azp_connected_component`.
        attr : :class:`numpy.ndarray`
            Refer to the corresponding argument in
            :meth:`AZP._azp_connected_component`.

        Returns
        -------
        labels : :class:`numpy.ndarray`
            Refer to the return value in :meth:`AZP._azp_connected_component`.
        """
        self.reset_tabu(1)
        # if there is only one region in the initial solution, just return it.
        distinct_regions = list(np.unique(initial_labels))
        if len(distinct_regions) == 1:
            return initial_labels

        #  step 2: make a list of the M regions
        labels = initial_labels

        it_since_tabu_len_changed = 0
        obj_val_start = float("inf")
        # step 12: Repeat steps 3-11 until either no further improvements are
        # made or a maximum number of iterations are exceeded.
        for it in range(self.maxit):
            obj_val_end = self.objective_func(labels, attr)
            if not obj_val_end < obj_val_start:
                break  # step 12
            obj_val_start = obj_val_end

            it_since_tabu_len_changed += 1
            # step 3: Define the list of all possible moves that are not tabu
            # and retain regional connectivity.
            possible_moves = []
            for area in range(labels.shape[0]):
                old_region = labels[area]
                sub_adj = sub_adj_matrix(
                    adj, np.where(labels == old_region)[0], wo_nodes=area)
                # moving the area must not destroy spatial contiguity in donor
                # region and if area is alone in its region, it must stay:
                if is_connected(sub_adj) and count(labels, old_region) > 1:
                    for neigh in neighbors(adj, area):
                        new_region = labels[neigh]
                        if new_region != old_region:
                            possible_move = Move(area, old_region, new_region)
                            if possible_move not in self.tabu:
                                possible_moves.append(possible_move)
            # step 4: Find the best nontabu move.
            best_move = None
            best_move_index = None
            best_objval_diff = float("inf")
            for i, move in enumerate(possible_moves):
                obj_val_diff = self.objective_func.update(
                    move.area, move.new_region, labels, attr)
                if obj_val_diff < best_objval_diff:
                    best_move_index, best_move = i, move
                    best_objval_diff = obj_val_diff
            # step 5: Make the move. Update the tabu status.
            self._make_move(best_move.area, best_move.new_region, labels)
            # step 6: Look up the current zoning system in a list of all zoning
            # systems visited so far during the search. If not found then go
            # to step 10.
            # Sets can't be permuted so we convert our list to a set:
            label_tup = tuple(labels)
            if label_tup in self.visited:
                # step 7: If it is found and it has been visited more than K1
                # times already and this cyclical behavior has been found on
                # at least K2 other occasions (involving other zones) then go
                # to step 11.
                times_visited = self.visited.count(label_tup)
                cycle = list(reversed(self.visited))
                cycle = cycle[:cycle.index(label_tup) + 1]
                cycle = list(reversed(cycle))
                it_until_repetition = len(cycle)
                if times_visited > self.k1:
                    times_cycle_found = 0
                    if self.k2 > 0:
                        for i in range(len(self.visited) - len(cycle)):
                            if self.visited[i:i + len(cycle)] == cycle:
                                times_cycle_found += 1
                                if times_cycle_found >= self.k2:
                                    break
                    if times_cycle_found >= self.k2:
                        # step 11: Delete all stored zoning systems and make P
                        # random moves, P = 1 + self.avg_it_until_rep/2, and
                        # update tabu to preclude a return to the previous
                        # state.
                        # we save the labels such that we can access it if
                        # this step yields a poor solution.
                        last_step = (11, tuple(labels))
                        self.visited = []
                        p = math.floor(1 + self.avg_it_until_rep / 2)
                        possible_moves.pop(best_move_index)
                        for _ in range(p):
                            move = possible_moves.pop(
                                random.randrange(len(possible_moves)))
                            self._make_move(move.area, move.new_region, labels)
                        continue
                    # step 8: Update a moving average of the repetition
                    # interval self.avg_it_until_rep, and increase the
                    # prohibition period R to 1.1*R.
                    self.rep_counter += 1
                    avg_it = self.avg_it_until_rep
                    self.avg_it_until_rep = 1 / self.rep_counter * \
                        ((self.rep_counter-1)*avg_it + it_until_repetition)

                    self.tabu = deque(self.tabu, 1.1 * self.tabu.maxlen)
                    # step 9: If the number of iterations since R was last
                    # changed exceeds self.avg_it_until_rep, then decrease R to
                    # max(0.9*R, 1).
                    if it_since_tabu_len_changed > self.avg_it_until_rep:
                        new_tabu_len = max([0.9 * self.tabu.maxlen, 1])
                        new_tabu_len = math.floor(new_tabu_len)
                        self.tabu = deque(self.tabu, new_tabu_len)
                    it_since_tabu_len_changed = 0  # step 8

            # step 10: Save the zoning system and go to step 12.
            self.visited.append(tuple(labels))
            last_step = 10

        if last_step == 10:
            try:
                return np.array(self.visited[-2])
            except IndexError:
                return np.array(self.visited[-1])
        # if step 11 was the last one, the result is in last_step[1]
        return np.array(last_step[1])
예제 #6
0
    def _azp_connected_component(self, adj, initial_clustering, attr):
        """
        Implementation of the basic tabu version of the AZP algorithm (refer
        to [OR1995]_) for a spatially connected set of areas (i.e. for every
        area there is a path to every other area).

        Parameters
        ----------
        adj : :class:`scipy.sparse.csr_matrix`
            Refer to the corresponding argument in
            :meth:`AZP._azp_connected_component`.
        initial_clustering : :class:`numpy.ndarray`
            Refer to the corresponding argument in
            :meth:`AZP._azp_connected_component`.
        attr : :class:`numpy.ndarray`
            Refer to the corresponding argument in
            :meth:`AZP._azp_connected_component`.

        Returns
        -------
        labels : :class:`numpy.ndarray`
            Refer to the return value in :meth:`AZP._azp_connected_component`.
        """
        self.reset_tabu()
        # if there is only one region in the initial solution, just return it.
        distinct_regions = list(np.unique(initial_clustering))
        if len(distinct_regions) == 1:
            return initial_clustering

        #  step 2: make a list of the M regions
        labels = initial_clustering

        visited = []
        stop = False
        while True:
            # added termination condition (not in Openshaw & Rao (1995))
            label_tup = tuple(labels)
            if visited.count(label_tup) >= self.reps_before_termination:
                stop = True

            visited.append(label_tup)

            # step 1 Find the global best move that is not prohibited or tabu.
            # find possible moves (globally)
            best_move = None
            best_objval_diff = float("inf")
            for area in range(labels.shape[0]):
                old_region = labels[area]
                sub_adj = sub_adj_matrix(
                    adj, np.where(labels == old_region)[0], wo_nodes=area)
                # moving the area must not destroy spatial contiguity in donor
                # region and if area is alone in its region, it must stay:
                if is_connected(sub_adj) and count(labels, old_region) > 1:
                    for neigh in neighbors(adj, area):
                        new_region = labels[neigh]
                        if new_region != old_region:
                            possible_move = Move(area, old_region, new_region)
                            if possible_move not in self.tabu:
                                objval_diff = self.objective_func.update(
                                    possible_move.area,
                                    possible_move.new_region, labels, attr)
                                if objval_diff < best_objval_diff:
                                    best_move = possible_move
                                    best_objval_diff = objval_diff
            # step 2: Make this move if it is an improvement or equivalet in
            # value.
            if best_move is not None and best_objval_diff <= 0:
                self._make_move(best_move.area, best_move.new_region, labels)
            else:
                # step 3: if no improving move can be made, then see if a tabu
                # move can be made which improves on the current local best
                # (termed an aspiration move)
                improving_tabus = [
                    move for move in self.tabu
                    if labels[move.area] == move.old_region
                    and self.objective_func.update(move.area, move.new_region,
                                                   labels, attr) < 0
                ]
                if improving_tabus:
                    aspiration_move = random_element_from(improving_tabus)
                    self._make_move(aspiration_move.area,
                                    aspiration_move.new_region, labels)
                else:
                    # step 4: If there is no improving move and no aspirational
                    # move, then make the best move even if it is nonimproving
                    # (that is, results in a worse value of the objective
                    # function).
                    if stop:
                        break
                    if best_move is not None:
                        self._make_move(best_move.area, best_move.new_region,
                                        labels)
        return labels
예제 #7
0
    def _azp_connected_component(self, adj, initial_clustering, attr):
        """
        Implementation of the AZP algorithm for a spatially connected set of
        areas (i.e. for every area there is a path to every other area).

        Parameters
        ----------
        adj : :class:`scipy.sparse.csr_matrix`
            Adjacency matrix representing the contiguity relation. The matrix'
            shape is (N, N) where N denotes the number of areas in the
            currently considered connected component.
        initial_clustering : :class:`numpy.ndarray`
            Array of labels. Shape: (N,) where N denotes the number of areas in
            the currently considered connected component.
        attr : :class:`numpy.ndarray`
            Array of labels. Shape: (N, M) where N denotes the number of areas
            in the currently considered connected component and M denotes the
            number of attributes per area.

        Returns
        -------
        labels : :class:`numpy.ndarray`
            One-dimensional array of region labels after the AZP algorithm has
            been performed. Only region labels of the currently considered
            connected component are returned.
        """
        # if there is only one region in the initial solution, just return it.
        distinct_regions = list(np.unique(initial_clustering))
        if len(distinct_regions) == 1:
            return initial_clustering
        distinct_regions_copy = distinct_regions.copy()

        #  step 2: make a list of the M regions
        labels = initial_clustering

        obj_val_start = float("inf")
        obj_val_end = self.allow_move_strategy.objective_val

        region_neighbors = {}
        for region in distinct_regions:
            region_areas = set(np.where(labels == region)[0])
            neighs = set()
            for area in region_areas:
                neighs.update(neighbors(adj, area))
            region_neighbors[region] = neighs.difference(region_areas)
        del neighs

        # step 7: Repeat until no further improving moves are made
        while obj_val_end < obj_val_start:  # improvement
            obj_val_start = float(obj_val_end)
            distinct_regions = distinct_regions_copy.copy()
            # step 6: when the list for region K is exhausted return to step 3
            # and select another region and repeat steps 4-6

            while distinct_regions:
                # step 3: select & remove any region K at random from this list
                recipient = pop_randomly_from(distinct_regions)
                while True:
                    # step 4: identify a set of zones bordering on members of
                    # region K that could be moved into region K without
                    # destroying the internal contiguity of the donor region(s)

                    candidates = []
                    for neigh in region_neighbors[recipient]:
                        neigh_region = labels[neigh]
                        sub_adj = sub_adj_matrix(
                            adj,
                            np.where(labels == neigh_region)[0],
                            wo_nodes=neigh)
                        if is_connected(sub_adj):
                            # if area is alone in its region, it must stay
                            if count(labels, neigh_region) > 1:
                                candidates.append(neigh)
                    # step 5: randomly select zones from this list until either
                    # there is a local improvement in the current value of the
                    # objective function or a move that is equivalently as good
                    # as the current best. Then make the move, update the list
                    # of candidate zones, and return to step 4 or else repeat
                    # step 5 until the list is exhausted.
                    while candidates:
                        cand = pop_randomly_from(candidates)
                        if self.allow_move_strategy(cand, recipient, labels):
                            donor = labels[cand]

                            make_move(cand, recipient, labels)

                            region_neighbors[donor].add(cand)
                            region_neighbors[recipient].discard(cand)

                            neighs_of_cand = neighbors(adj, cand)

                            recipient_region_areas = set(
                                np.where(labels == recipient)[0])
                            region_neighbors[recipient].update(neighs_of_cand)
                            region_neighbors[recipient].difference_update(
                                recipient_region_areas)

                            donor_region_areas = set(
                                np.where(labels == donor)[0])
                            not_donor_neighs_anymore = set(
                                area for area in neighs_of_cand if not any(
                                    a in donor_region_areas
                                    for a in neighbors(adj, area)))
                            region_neighbors[donor].difference_update(
                                not_donor_neighs_anymore)
                            break
                    else:
                        break

            obj_val_end = float(self.allow_move_strategy.objective_val)
        return labels