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
Ejemplo n.º 5
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)