def test_pop_randomly_from(): lst = list(range(1000)) n_pops = 5 popped = [] for _ in range(n_pops): lst_copy = list(lst) popped.append(util.pop_randomly_from(lst_copy)) assert len(lst_copy) == len(lst) - 1 assert not_all_elements_equal(popped)
def assign_enclaves(self, partition, enclave_areas, neigh_dict, attr): """ Start with a partial partition (not all areas are assigned to a region) and a list of enclave areas (i.e. areas not present in the partial partition). Then assign all enclave areas to regions in the partial partition and return the resulting partition. Parameters ---------- partition : `list` Each element (of type `set`) represents a region. enclave_areas : `list` Each element represents an area. neigh_dict : `dict` Each key represents an area. Each value is an iterable of the corresponding neighbors. attr : :class:`numpy.ndarray` See the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. Returns ------- partition : `list` Each element (of type `set`) represents a region. """ # print("partition:", partition, "- enclaves:", enclave_areas) while enclave_areas: neighbors_of_assigned = [ area for area in enclave_areas if any( neigh not in enclave_areas for neigh in neigh_dict[area]) ] area = pop_randomly_from(neighbors_of_assigned) neigh_regions_idx = [] for neigh in neigh_dict[area]: try: neigh_regions_idx.append( find_sublist_containing(neigh, partition, index=True)) except LookupError: pass region_idx = self.find_best_region_idx(area, partition, neigh_regions_idx, attr) partition[region_idx].add(area) enclave_areas.remove(area) return partition
def grow_regions(self, adj, attr, spatially_extensive_attr, threshold): """ Parameters ---------- adj : :class:`scipy.sparse.csr_matrix` See the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. attr : :class:`numpy.ndarray` See the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. spatially_extensive_attr : :class:`numpy.ndarray` See the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. threshold : numbers.Real or :class:`numpy.ndarray` See the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. Returns ------- result : `tuple` `result[0]` is a `list`. Each list element is a `set` of a region's areas. Note that not every area is assigned to a region after this function has terminated, so they won't be in any of the `set`s in `result[0]`. `result[1]` is a `list` of areas not assigned to any region. """ # print("grow_regions called with spatially_extensive_attr", spatially_extensive_attr) partition = [] enclave_areas = [] unassigned_areas = list(range(adj.shape[0])) assigned_areas = [] # todo: rm prints while unassigned_areas: # print("partition", partition) area = pop_randomly_from(unassigned_areas) # print("seed in area", area) assigned_areas.append(area) if (spatially_extensive_attr[area] >= threshold).all(): # print(" seed --> region :)") # print("because", spatially_extensive_attr[area], ">=", threshold) partition.append({area}) else: region = {area} # print(" all neighbors:", neigh_dict[area]) # print(" already assigned:", assigned_areas) unassigned_neighs = set( adj[area].nonzero()[1]).difference(assigned_areas) feasible = True spat_ext_attr = spatially_extensive_attr[area].copy() while (spat_ext_attr < threshold).any(): # print(" ", spat_ext_attr, "<", threshold, "Need neighs!") # print(" potential neighbors:", unassigned_neighs) if unassigned_neighs: neigh = self.find_best_area(region, unassigned_neighs, attr) # print(" we choose neighbor", neigh) region.add(neigh) unassigned_neighs.remove(neigh) unassigned_neighs.update(set(adj[neigh].nonzero()[1])) unassigned_neighs.difference_update(assigned_areas) spat_ext_attr += spatially_extensive_attr[neigh] unassigned_areas.remove(neigh) assigned_areas.append(neigh) else: # print(" Oh no! No neighbors left :(") enclave_areas.extend(region) feasible = False # the following line (present in the algorithm in # [DAR2012]) is commented out because it leads to an # infinite loop: # unassigned_areas.extend(region) for area in region: assigned_areas.remove(area) break if feasible: partition.append(region) # print(" unassigned:", unassigned_areas) # print(" assigned:", assigned_areas) # print() # print("grow_regions partit.:", partition, "enclaves:", enclave_areas) return partition, enclave_areas
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
def grow_regions(self, adj, attr, spatially_extensive_attr, threshold): """ Parameters ---------- adj : :class:`scipy.sparse.csr_matrix` See the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. attr : :class:`numpy.ndarray` See the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. spatially_extensive_attr : :class:`numpy.ndarray` See the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. threshold : numbers.Real or :class:`numpy.ndarray` See the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. Returns ------- result : `tuple` `result[0]` is a `list`. Each list element is a `set` of a region's areas. Note that not every area is assigned to a region after this function has terminated, so they won't be in any of the `set`s in `result[0]`. `result[1]` is a `list` of areas not assigned to any region. """ partition = [] enclave_areas = [] unassigned_areas = list(range(adj.shape[0])) assigned_areas = [] while unassigned_areas: area = pop_randomly_from(unassigned_areas) assigned_areas.append(area) if (spatially_extensive_attr[area] >= threshold).all(): partition.append({area}) else: region = {area} unassigned_neighs = set( adj[area].nonzero()[1]).difference(assigned_areas) feasible = True spat_ext_attr = spatially_extensive_attr[area].copy() while (spat_ext_attr < threshold).any(): if unassigned_neighs: neigh = self.find_best_area(region, unassigned_neighs, attr) region.add(neigh) unassigned_neighs.remove(neigh) unassigned_neighs.update(set(adj[neigh].nonzero()[1])) unassigned_neighs.difference_update(assigned_areas) spat_ext_attr += spatially_extensive_attr[neigh] unassigned_areas.remove(neigh) assigned_areas.append(neigh) else: enclave_areas.extend(region) feasible = False # the following line (present in the algorithm in # [DAR2012]) is commented out because it leads to an # infinite loop: # unassigned_areas.extend(region) for area in region: assigned_areas.remove(area) break if feasible: partition.append(region) return partition, enclave_areas