e = edges.pop() attr_lengths[e] = (len(G.node_attributes(e[0])) + len(G.node_attributes(e[1]))) contract(G, min(attr_lengths)) communities = [Community(i, precinct_graph) for i in range(n_communities)] for i, node in enumerate(G.nodes()): for precinct_node in G.node_attributes(node): communities[i].take_precinct( precinct_graph.node_attributes(precinct_node)[0]) return communities if __name__ == "__main__": # Load input file and calculate initial configuration. with open(sys.argv[1], "rb") as f: precinct_graph = pickle.load(f) communities = create_initial_configuration(precinct_graph, int(sys.argv[2])) # Write to file. with open(sys.argv[3], "wb+") as f: pickle.dump(communities, f) # Display and/or save image. image_output = sys.argv[4] if sys.argv[4] != "none" else None if image_output is not None: draw_state(precinct_graph, None, fpath=image_output)
def optimize_population_stdev(communities, graph, animation_dir=None): """Takes a set of communities and exchanges precincts such that the standard deviation of the populations of their precincts are as low as possible. :param communities: The current state of the communities within a state. :type communities: list of `hacking_the_election.utils.community.Community` :param graph: A graph containing precinct data within a state. :type graph: `pygraph.classes.graph.graph` :param animation_dir: Path to the directory where animation files should be saved, defaults to None :type animation_dir: str or NoneType """ for community in communities: community.update_population_stdev() if animation_dir is not None: draw_state(graph, animation_dir) best_communities = [copy.copy(c) for c in communities] last_communities = [] while True: # Find most inconsistent population community. community = max(communities, key=lambda c: c.population_stdev) iteration_stdevs = [c.population_stdev for c in communities] avg_stdev = average(iteration_stdevs) rounded_stdevs = [round(stdev, 3) for stdev in iteration_stdevs] print(rounded_stdevs, round(avg_stdev, 3)) if avg_stdev > average([c.population_stdev for c in best_communities]): best_communities = [copy.copy(c) for c in communities] if last_communities != []: # Check if anything changed. If not, exit the function. changed = False try: for c in communities: for c2 in last_communities: if c.id == c2.id: if c.precincts != c2.precincts: changed = True raise LoopBreakException except LoopBreakException: pass if not changed: # Revert to best community state. for c in best_communities: for c2 in communities: if c.id == c2.id: c2 = c for c in communities: for precinct in c.precincts.values(): precinct.community = c.id rounded_stdevs = [ round(c.population_stdev, 3) for c in communities ] avg_stdev = average([c.population_stdev for c in communities]) print(rounded_stdevs, round(avg_stdev, 3)) if animation_dir is not None: draw_state(graph, animation_dir) return # Not deepcopy so that precinct objects are not copied (saves memory). last_communities = [copy.copy(c) for c in communities] giveable_precincts = get_giveable_precincts(graph, communities, community.id) for precinct, other_community in giveable_precincts: starting_stdev = average([c.population_stdev for c in communities]) community.give_precinct(other_community, precinct.id, update={"population_stdev"}) # Check if exchange made `community` non-contiguous. # Or if it hurt the previous population stdev. if (len(get_components(community.induced_subgraph)) > 1 or community.population_stdev > starting_stdev): other_community.give_precinct(community, precinct.id, update={"population_stdev"}) else: average_stdev = average( [c.population_stdev for c in communities]) if average_stdev > starting_stdev: other_community.give_precinct(community, precinct.id, update={"population_stdev"}) else: if animation_dir is not None: draw_state(graph, animation_dir) takeable_precincts = get_takeable_precincts(graph, communities, community.id) for precinct, other_community in takeable_precincts: starting_stdev = average([c.population_stdev for c in communities]) other_community.give_precinct(community, precinct.id, update={"population_stdev"}) # Check if exchange made `community` non-contiguous. # Or if it hurt the previous population stdev. if len(get_components(other_community.induced_subgraph)) > 1: community.give_precinct(other_community, precinct.id, update={"population_stdev"}) else: average_stdev = average( [c.population_stdev for c in communities]) if average_stdev > starting_stdev: community.give_precinct(other_community, precinct.id, update={"population_stdev"}) else: if animation_dir is not None: draw_state(graph, animation_dir)
# TO LOAD INIT CONFIG FROM TEXT FILE: # with open("test_vermont_init_config.txt", "r") as f: # precinct_list = eval(f.read()) # communities = [] # from hacking_the_election.utils.community import Community # for i, community in enumerate(precinct_list): # c = Community(i, graph) # for precinct_id in community: # for node in graph.nodes(): # precinct = graph.node_attributes(node)[0] # if precinct.id == precinct_id: # c.take_precinct(precinct) # communities.append(c) animation_dir = None if sys.argv[3] == "none" else sys.argv[3] if animation_dir is not None: try: os.mkdir(animation_dir) except FileExistsError: pass start_time = time.time() optimize_compactness(communities, graph, animation_dir) print(f"Compactness optimization took {time.time() - start_time} seconds.") draw_state(graph, None, fpath="test_thing3.png") with open(sys.argv[4], "wb+") as f: pickle.dump(communities, f)
def optimize_compactness(communities, graph, animation_dir=None): """Takes a set of communities and exchanges precincts in a way that maximizes compactness. :param communities: The current state of the communities within a state. :type communities: list of `hacking_the_election.utils.community.Community` :param graph: A graph containing precinct data within a state. :type graph: `pygraph.classes.graph.graph` :param animation_dir: Path to the directory where animation files should be saved, defaults to None :type animation_dir: str or NoneType """ for community in communities: community.update_imprecise_compactness() if animation_dir is not None: draw_state(graph, animation_dir) best_communities = [copy.copy(c) for c in communities] last_communities = [] iterations_since_best = 0 n = 0 while True: # Stop if number of iterations since the best # communities so far is more than N. if min([c.imprecise_compactness for c in communities]) \ > min([c.imprecise_compactness for c in best_communities]): # Current communities are new best. best_communities = [copy.copy(c) for c in communities] iterations_since_best = 0 else: iterations_since_best += 1 if iterations_since_best > N: # Revert to best and exit function. for c in communities: for bc in best_communities: if c.id == bc.id: c = bc rounded_compactnesses = [round(c.imprecise_compactness, 3) for c in communities] print(rounded_compactnesses, min(rounded_compactnesses)) return # Stop if there has been no change in communities this iteration. if last_communities != []: try: for c in communities: for lc in last_communities: if c.id == lc.id: if c.precincts != lc.precincts: changed = True raise LoopBreakException # Revert to best and exit function. # (LoopBreakException was not raised, # so communities did not change). for c in communities: for bc in best_communities: if c.id == bc.id: c = bc rounded_compactnesses = [round(c.imprecise_compactness, 3) for c in communities] print(rounded_compactnesses, min(rounded_compactnesses)) return except LoopBreakException: pass community = communities[n] # Precinct centroid coords X = [] Y = [] precinct_areas = [] for p in community.precincts.values(): X.append(p.centroid[0]) Y.append(p.centroid[1]) precinct_areas.append(p.coords.area) center = [average(X), average(Y)] radius = math.sqrt(sum(precinct_areas) / math.pi) for _ in range(M): # Communities that have exchanged precincts with `community` other_communities = set() giveable_precincts = get_giveable_precincts( graph, communities, community.id) for precinct, other_community in giveable_precincts: if get_distance(precinct.centroid, center) > radius: community.give_precinct( other_community, precinct.id) if len(get_components(community.induced_subgraph)) > 1: # Giving precinct made `community` non contiguous. other_community.give_precinct( community, precinct.id) else: other_communities.add(other_community) takeable_precincts = get_takeable_precincts( graph, communities, community.id) for precinct, other_community in takeable_precincts: if get_distance(precinct.centroid, center) <= radius: other_community.give_precinct( community, precinct.id) if len(get_components(other_community.induced_subgraph)) > 1: # Giving precicnt made `other_community` non contiguous. community.give_precinct( other_community, precinct.id) else: other_communities.add(other_community) for other_community in other_communities: other_community.update_imprecise_compactness() other_community.update_imprecise_compactness() rounded_compactnesses = [round(c.imprecise_compactness, 3) for c in communities] print(rounded_compactnesses, min(rounded_compactnesses)) n += 1 if n == len(communities): n = 0
def optimize_population(communities, graph, percentage, animation_dir=None): """Takes a set of communities and exchanges precincts so that the population is as evenly distributed as possible. :param communities: The current state of the communities within a state. :type communities: list of `hacking_the_election.utils.community.Community` :param graph: A graph containing precinct data within a state. :type graph: `pygraph.classes.graph.graph` :param percentage: By how much the populations are able to deviate from one another. A value of 1% means that all the communities must be within 0.5% of the ideal population, so that they are guaranteed to be within 1% of each other. :type percentage: float between 0 and 1 :param animation_dir: Path to the directory where animation files should be saved, defaults to None :type animation_dir: str or NoneType """ for community in communities: community.update_population() if animation_dir is not None: draw_state(graph, animation_dir) ideal_population = average([c.population for c in communities]) print(f"{ideal_population=}") while not _check_communities_complete(communities, percentage): community = max(communities, key=lambda c: abs(c.population - ideal_population)) if community.population > ideal_population: giveable_precincts = get_giveable_precincts( graph, communities, community.id) # Weight random choice by distance from community centroid. community_centroid = community.centroid giveable_precinct_weights = [ get_distance(pair[0].centroid, community_centroid) for pair in giveable_precincts ] max_distance = max(giveable_precinct_weights) giveable_precinct_weights = \ [max_distance - weight for weight in giveable_precinct_weights] while community.population > ideal_population: if giveable_precincts == []: giveable_precincts = get_giveable_precincts( graph, communities, community.id) # Weight random choice by distance from community centroid. community_centroid = community.centroid giveable_precinct_weights = [ get_distance(pair[0].centroid, community_centroid) for pair in giveable_precincts ] max_distance = max(giveable_precinct_weights) giveable_precinct_weights = \ [max_distance - weight for weight in giveable_precinct_weights] # Choose random precinct and community and remove from lists. precinct, other_community = random.choices( giveable_precincts, weights=giveable_precinct_weights)[0] giveable_precinct_weights.pop( giveable_precincts.index((precinct, other_community))) giveable_precincts.remove((precinct, other_community)) community.give_precinct(other_community, precinct.id, update={"population"}) if len(get_components(community.induced_subgraph)) > 1: # Give back precinct. other_community.give_precinct(community, precinct.id, update={"population"}) elif community.population < ideal_population: takeable_precincts = get_takeable_precincts( graph, communities, community.id) # Weight random choice by distance from community centroid. community_centroid = community.centroid takeable_precinct_weights = [ get_distance(pair[0].centroid, community_centroid) for pair in takeable_precincts ] while community.population < ideal_population: if takeable_precincts == []: takeable_precincts = get_takeable_precincts( graph, communities, community.id) # Weight random choice by distance from community centroid. community_centroid = community.centroid takeable_precinct_weights = [ get_distance(pair[0].centroid, community_centroid) for pair in takeable_precincts ] precinct, other_community = random.choices( takeable_precincts, weights=takeable_precinct_weights)[0] takeable_precinct_weights.pop( takeable_precincts.index((precinct, other_community))) takeable_precincts.remove((precinct, other_community)) other_community.give_precinct(community, precinct.id, update={"population"}) if len(get_components(other_community.induced_subgraph)) > 1: # Give back precinct. community.give_precinct(other_community, precinct.id, update={"population"}) if animation_dir is not None: draw_state(graph, animation_dir) print([c.population for c in communities], community.population)