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
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
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
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
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])
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
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