示例#1
0
def test_election_result_has_a_cute_str_method():
    election = Election("2008 Presidential", {
        "Democratic": [3, 1, 2],
        "Republican": [1, 2, 1]
    })
    results = ElectionResults(
        election,
        {
            "Democratic": {
                0: 3,
                1: 1,
                2: 2
            },
            "Republican": {
                0: 1,
                1: 2,
                2: 1
            }
        },
        [0, 1, 2],
    )
    expected = ("Election Results for 2008 Presidential\n"
                "0:\n"
                "  Democratic: 0.75\n"
                "  Republican: 0.25\n"
                "1:\n"
                "  Democratic: 0.3333\n"
                "  Republican: 0.6667\n"
                "2:\n"
                "  Democratic: 0.6667\n"
                "  Republican: 0.3333")
    assert str(results) == expected
def main(config_data, id):
    try:
        timeBeg = time.time()
        print('Experiment', id, 'has begun')
        # Save configuration into global variable
        global config
        config = config_data

        graph = Graph.from_json(config['INPUT_GRAPH_FILENAME'])

        # List of districts in original graph
        parts = list(
            set([
                graph.nodes[node][config['ASSIGN_COL']]
                for node in graph.nodes()
            ]))
        # Ideal population of districts
        ideal_pop = sum(
            [graph.nodes[node][config['POP_COL']]
             for node in graph.nodes()]) / len(parts)

        election = Election(config['ELECTION_NAME'], {
            'PartyA': config['PARTY_A_COL'],
            'PartyB': config['PARTY_B_COL']
        })

        updaters = {
            'population': Tally(config['POP_COL']),
            'cut_edges': cut_edges,
            config['ELECTION_NAME']: election
        }

        partDict = recursive_tree_part(graph, parts, ideal_pop,
                                       config['POP_COL'], config['EPSILON'],
                                       config['NODE_REPEATS'])
        for node in graph.nodes():
            graph.nodes[node][config['ASSIGN_COL']] = partDict[node]
        part = Partition(graph=graph,
                         assignment=config['ASSIGN_COL'],
                         updaters=updaters)
        for len_ in config['RUN_LENGTHS']:
            for num in range(config['RUNS_PER_LEN']):
                run_chain(part, config['CHAIN_TYPE'], len_, ideal_pop,
                          '{}_{}_{}_{}'.format(config['TAG'], id, len_, num))

        print('Experiment {} completed in {} seconds'.format(
            id,
            time.time() - timeBeg))

    except Exception as e:
        # Print notification if any experiment fails to complete
        track = traceback.format_exc()
        print(track)
        print('Experiment {} failed to complete after {:.2f} seconds'.format(
            id,
            time.time() - timeBeg))
示例#3
0
def partition_with_election(graph_with_d_and_r_cols):
    graph = graph_with_d_and_r_cols
    assignment = random_assignment(graph, 3)
    parties_to_columns = {
        "D": {node: graph.nodes[node]["D"]
              for node in graph.nodes},
        "R": {node: graph.nodes[node]["R"]
              for node in graph.nodes},
    }
    election = Election("Mock Election", parties_to_columns)
    updaters = {"Mock Election": election, "cut_edges": cut_edges}
    return Partition(graph, assignment, updaters)
示例#4
0
def partition_with_election(graph_with_d_and_r_cols):
    graph = graph_with_d_and_r_cols
    assignment = random_assignment(graph, 3)
    parties_to_columns = {
        'D': {node: graph.nodes[node]['D']
              for node in graph.nodes},
        'R': {node: graph.nodes[node]['R']
              for node in graph.nodes}
    }
    election = Election("Mock Election", parties_to_columns)
    updaters = {"Mock Election": election}
    return Partition(graph, assignment, updaters)
示例#5
0
def test_vote_proportion_returns_nan_if_total_votes_is_zero(
        three_by_three_grid):
    election = Election("Mock Election", ["D", "R"], alias="election")
    graph = three_by_three_grid

    for node in graph.nodes:
        for col in election.columns:
            graph.nodes[node][col] = 0

    updaters = {"election": election}
    assignment = random_assignment(graph, 3)

    partition = Partition(graph, assignment, updaters)

    assert all(
        math.isnan(value) for party_percents in
        partition["election"].percents_for_party.values()
        for value in party_percents.values())
    graph.nodes[node][P1] = 0
    graph.nodes[node][P2] = 1
    
vote_dict = {1: P1, 0: P2}

def step_num(partition):
    parent = partition.parent
    if not parent:
        return 0
    return parent["step_num"] + 1

updaters = {
    "population": Tally("population"),
    "cut_edges": cut_edges,
    "step_num": step_num,
    "P1 vs P2": Election("P1 vs P2", {"P1": P1, "P2": P2}),}

part_dict = {x: int(x[0]/(grid_width/k_partitions))
             
for x in graph.nodes()}
    init_part = Partition(graph, assignment=part_dict, updaters=updaters)
    
def p1_wins(partition):
    if partition["P1 vs P2"].wins("P1")+partition["P1 vs P2"].wins("P2") > k_partitions:
        return equal_check(partition, "P1")
    else:
        return (partition["P1 vs P2"].wins("P1")/k_partitions)*100
    
def p2_wins(partition):
    if partition["P1 vs P2"].wins("P1")+partition["P1 vs P2"].wins("P2") > k_partitions:
        return equal_check(partition, "P2")
def produce_gerrymanders(graph, k, tag, sample_size, chaintype):
    # Samples k-partitions of the graph
    # stores vote histograms, and returns most extreme partitions.
    for node in graph.nodes():
        graph.nodes[node]["last_flipped"] = 0
        graph.nodes[node]["num_flips"] = 0

    ideal_population = sum(graph.nodes[x][config["POPULATION_COLUMN"]] for x in graph.nodes()) / k
    election = Election(
        config['ELECTION_NAME'],
        {'PartyA': config['PARTY_A_COL'], 'PartyB': config['PARTY_B_COL']},
        alias=config['ELECTION_ALIAS']
    )
    updaters = {'population': Tally(config['POPULATION_COLUMN']),
                'cut_edges': cut_edges,
                'step_num': step_num,
                config['ELECTION_ALIAS'] : election
                }
    initial_partition = Partition(graph, assignment=config['ASSIGNMENT_COLUMN'], updaters=updaters)
    popbound = within_percent_of_ideal_population(initial_partition, config['POPULATION_EPSILON'])

    if chaintype == "tree":
        tree_proposal = partial(recom, pop_col=config["POPULATION_COLUMN"], pop_target=ideal_population,
                           epsilon=config['POPULATION_EPSILON'], node_repeats=config['NODE_REPEATS'],
                           method=facefinder.my_mst_bipartition_tree_random)

    elif chaintype == "uniform_tree":
        tree_proposal = partial(recom, pop_col=config["POPULATION_COLUMN"], pop_target=ideal_population,
                           epsilon=config['POPULATION_EPSILON'], node_repeats=config['NODE_REPEATS'],
                           method=facefinder.my_uu_bipartition_tree_random)
    else:
        print("Chaintype used: ", chaintype)
        raise RuntimeError("Chaintype not recognized. Use 'tree' or 'uniform_tree' instead")

    exp_chain = MarkovChain(tree_proposal, Validator([popbound]), accept=accept.always_accept, initial_state=initial_partition,
                            total_steps=sample_size)

    seats_won_table = []
    best_left = np.inf
    best_right = -np.inf
    for ctr, part in enumerate(exp_chain):
        seats_won = 0

        if ctr % 100 == 0:
            print("step ", ctr)
        for i in range(k):
            rep_votes = 0
            dem_votes = 0
            for node in graph.nodes():
                if part.assignment[node] == i:
                    rep_votes += graph.nodes[node]["EL16G_PR_R"]
                    dem_votes += graph.nodes[node]["EL16G_PR_D"]
            total_seats = int(rep_votes > dem_votes)
            seats_won += total_seats
        # total seats won by rep
        seats_won_table.append(seats_won)
        # save gerrymandered partitions
        if seats_won < best_left:
            best_left = seats_won
            left_mander = copy.deepcopy(part.parts)
        if seats_won > best_right:
            best_right = seats_won
            right_mander = copy.deepcopy(part.parts)
        # print("finished round"

    print("max", best_right, "min:", best_left)

    plt.figure()
    plt.hist(seats_won_table, bins=10)

    name = "./plots/large_sample/seats_hist/seats_histogram_orig" + tag + ".png"
    plt.savefig(name)
    plt.close()
    return left_mander, right_mander

def b_nodes_bi(partition):
    return {x[0]
            for x in partition["cut_edges"]
            }.union({x[1]
                     for x in partition["cut_edges"]})


updaters = {
    "population": Tally("TOT_POP"),
    "cut_edges": cut_edges,
    "step_num": step_num,
    'b_nodes': b_nodes_bi,
    'SEN16': Election('SEN16', {
        'DEM': 'T16SEND',
        'GOP': 'T16SENR'
    })
}

cols = [
    '2011_PLA_1', 'GOV', 'TS', 'REMEDIAL_P', '538CPCT__1', '538DEM_PL',
    '538GOP_PL', '8THGRADE_1'
]

Parts = [Partition(g, cddicts[i], updaters) for i in range(num_trees)]
for col in cols:
    Parts.append(Partition(g, col, updaters))

for i in range(num_trees):
    cols.insert(0, f'Tree{i}')
示例#9
0
            neighbor = (vertex[0] + 1, vertex[1])
            if assign[vertex] != assign[neighbor]:
                cut_edge_set.add((vertex, neighbor))
        if vertex[1] < n - 1:
            neighbor = (vertex[0], vertex[1] + 1)
            if assign[vertex] != assign[neighbor]:
                cut_edge_set.add((vertex, neighbor))
    return cut_edge_set


updaters = {
    "population": Tally("population"),
    "cut_edges": cut_edges,
    "step_num": step_num,
    "Pink-Purple": Election("Pink-Purple", {
        "Pink": "pink",
        "Purple": "purple"
    }),
    "rook_cut_edges": rook_cut_edges
}

# ########BUILD PARTITION

grid_partition = Partition(graph, assignment=cddict, updaters=updaters)

# ADD CONSTRAINTS
# FOr our 10x10 grid, will only allow districts of exactly 10 vertices
popbound = within_percent_of_ideal_population(grid_partition, 0.1)

# ########Setup Proposal
ideal_population = sum(
    grid_partition["population"].values()) / len(grid_partition)
示例#10
0
# ###CONFIGURE UPDATERS


def step_num(partition):
    parent = partition.parent
    if not parent:
        return 0
    return parent["step_num"] + 1


updaters = {
    "population": Tally("population"),
    "cut_edges": cut_edges,
    "step_num": step_num,
    "Pink-Purple": Election("Pink-Purple", {"Pink": "pink", "Purple": "purple"}),
        "Green-Yellow": Election("Green-Yellow", {"green": "green", "yellow": "yellow"}),
    'b_nodes':b_nodes,
}


# ########BUILD PARTITION

grid_partition = Partition(graph, assignment=cddict, updaters=updaters)

# ADD CONSTRAINTS
popbound = within_percent_of_ideal_population(grid_partition, 0.1)

# ########Setup Proposal
ideal_population = sum(grid_partition["population"].values()) / len(grid_partition)
示例#11
0
def main():
    """ Contains majority of expermiment. Runs a markov chain on the state dual graph, determining how the distribution is affected to changes in the
     state dual graph.
     Raises:
        RuntimeError if PROPOSAL_TYPE of config file is neither 'sierpinski'
        nor 'convex'
    """
    output_directory = createDirectory(config)
    epsilon = config["epsilon"]
    k = config["NUM_DISTRICTS"]
    updaters = {'population': Tally('population'),
                'cut_edges': cut_edges,
                "Blue-Red": Election("Blue-Red", {"Blue": "blue", "Red": "red"})
                }
    graph, dual = preprocessing(output_directory)
    cddict = {x: int(x[0] / gn) for x in graph.nodes()}

    plt.figure()
    nx.draw(
        graph,
        pos={x: x for x in graph.nodes()},
        node_color=[cddict[x] for x in graph.nodes()],
        node_size=ns,
        node_shape="s",
        cmap="tab20",
    )
    plt.show()

    ideal_population = sum(graph.nodes[x]["population"] for x in graph.nodes()) / k
    faces = graph.graph["faces"]
    faces = list(faces)
    square_faces = [face for face in faces if len(face) == 4]
    totpop = 0
    for node in graph.nodes():
        totpop += int(graph.nodes[node]['population'])
    # length of chain
    steps = config["CHAIN_STEPS"]

    # length of each gerrychain step
    gerrychain_steps = config["GERRYCHAIN_STEPS"]
    # faces that are currently modified. Code maintains list of modified faces, and at each step selects a face. if face is already in list,
    # the face is un-modified, and if it is not, the face is modified by the specified proposal type.
    special_faces = set([face for face in square_faces if np.random.uniform(0, 1) < .5])
    #chain_output = {'dem_seat_data': [], 'rep_seat_data': [], 'score': []}
    chain_output = defaultdict(list)

    # start with small score to move in right direction
    print("Choosing", math.floor(len(faces) * config['PERCENT_FACES']), "faces of the dual graph at each step")
    max_score = -math.inf
    # this is the main markov chain
    for i in tqdm.tqdm(range(1, steps + 1), ncols=100, desc="Chain Progress"):
        special_faces_proposal = copy.deepcopy(special_faces)
        proposal_graph = copy.deepcopy(graph)
        if (config["PROPOSAL_TYPE"] == "sierpinski"):
            for i in range(math.floor(len(faces) * config['PERCENT_FACES'])):
                face = random.choice(faces)
                ##Makes the Markov chain lazy -- this just makes the chain aperiodic.
                if random.random() > .5:
                    if not (face in special_faces_proposal):
                        special_faces_proposal.append(face)
                    else:
                        special_faces_proposal.remove(face)
            chain_on_subsets_of_faces.face_sierpinski_mesh(proposal_graph, special_faces_proposal)
        elif (config["PROPOSAL_TYPE"] == "add_edge"):
            for j in range(math.floor(len(square_faces) * config['PERCENT_FACES'])):
                face = random.choice(square_faces)
                ##Makes the Markov chain lazy -- this just makes the chain aperiodic.
                if random.random() > .5:
                    if not (face in special_faces_proposal):
                        special_faces_proposal.add(face)
                    else:
                        special_faces_proposal.remove(face)
            chain_on_subsets_of_faces.add_edge_proposal(proposal_graph, special_faces_proposal)
        else:
            raise RuntimeError('PROPOSAL TYPE must be "sierpinski" or "convex"')

        initial_partition = Partition(proposal_graph, assignment=cddict, updaters=updaters)

        # Sets up Markov chain
        popbound = within_percent_of_ideal_population(initial_partition, epsilon)
        tree_proposal = partial(recom, pop_col=config['POP_COL'], pop_target=ideal_population, epsilon=epsilon,
                                node_repeats=1)

        # make new function -- this computes the energy of the current map
        exp_chain = MarkovChain(tree_proposal, Validator([popbound]), accept=accept.always_accept,
                                initial_state=initial_partition, total_steps=gerrychain_steps)
        seats_won_for_republicans = []
        seats_won_for_democrats = []
        for part in exp_chain:
            for u, v in part["cut_edges"]:
                proposal_graph[u][v]["cut_times"] += 1

            rep_seats_won = 0
            dem_seats_won = 0
            for j in range(k):
                rep_votes = 0
                dem_votes = 0
                for n in graph.nodes():
                    if part.assignment[n] == j:
                        rep_votes += graph.nodes[n]["blue"]
                        dem_votes += graph.nodes[n]["red"]
                total_seats_dem = int(dem_votes > rep_votes)
                total_seats_rep = int(rep_votes > dem_votes)
                rep_seats_won += total_seats_rep
                dem_seats_won += total_seats_dem
            seats_won_for_republicans.append(rep_seats_won)
            seats_won_for_democrats.append(dem_seats_won)

        seat_score = statistics.mean(seats_won_for_republicans)

        # implement mattingly simulated annealing scheme, from evaluating partisan gerrymandering in wisconsin
        if i <= math.floor(steps * .67):
            beta = i / math.floor(steps * .67)
        else:
            beta = (i / math.floor(steps * 0.67)) * 100
        temperature = 1 / (beta)

        weight_seats = 1
        weight_flips = -.2
        config['PERCENT_FACES'] = config['PERCENT_FACES']  # what is this?
        flip_score = len(special_faces)  # This is the number of edges being swapped

        score = weight_seats * seat_score + weight_flips * flip_score

        ##This is the acceptance step of the Metropolis-Hasting's algorithm. Specifically, rand < min(1, P(x')/P(x)), where P is the energy and x' is proposed state
        # if the acceptance criteria is met or if it is the first step of the chain
        def update_outputs():
            chain_output['dem_seat_data'].append(seats_won_for_democrats)
            chain_output['rep_seat_data'].append(seats_won_for_republicans)
            chain_output['score'].append(score)
            chain_output['seat_score'].append(seat_score)
            chain_output['flip_score'].append(flip_score)


        def propagate_outputs():
            for key in chain_output.keys():
                chain_output[key].append(chain_output[key][-1])

        if i == 1:
            update_outputs()
            special_faces = copy.deepcopy(special_faces_proposal)
        # this is the simplified form of the acceptance criteria, for intuitive purposes
        # exp((1/temperature) ( proposal_score - previous_score))
        elif np.random.uniform(0, 1) < (math.exp(score) / math.exp(chain_output['score'][-1])) ** (1 / temperature):
            update_outputs()
            special_faces = copy.deepcopy(special_faces_proposal)
        else:
            propagate_outputs()

        # if score is highest seen, save map.
        if score > max_score:
            # todo: all graph coloring for graph changes that produced this score
            nx.write_gpickle(proposal_graph, "obj/graphs/"+str(score)+'sc_'+str(config['CHAIN_STEPS'])+'mcs_'+ str(config["GERRYCHAIN_STEPS"])+ "gcs_" +
                config['PROPOSAL_TYPE']+'_'+ str(len(special_faces)), pickle.HIGHEST_PROTOCOL)
            save_obj(special_faces, output_directory, 'north_carolina_highest_found')
            nx.write_gpickle(proposal_graph, output_directory + '/' +  "max_score", pickle.HIGHEST_PROTOCOL)
            f=open(output_directory + "/max_score_data.txt","w+")
            f.write("maximum score: " + str(score) + "\n" + "edges changed: " + str(len(special_faces)) + "\n" + "Seat Score: " + str(seat_score))
            save_obj(special_faces, output_directory + '/', "special_faces")
            max_score = score

    plt.plot(range(len(chain_output['score'])), chain_output['score'])
    plt.xlabel("Meta-Chain Step")
    plt.ylabel("Score")
    plt.show()
    plt.close()

    plt.plot(range(len(chain_output['seat_score'])), chain_output['seat_score'])
    plt.xlabel("Meta-Chain Step")
    plt.ylabel("Number of average seats republicans won")
    plt.show()
    plt.close()
def run_ensemble_on_distro(graph, min_pop_col, maj_pop_col, tot_pop_col, num_districts, initial_plan, num_steps, pop_tol = 0.05, min_win_thresh = 0.5):
    """Runs a Recom chain on a given graph with a given minority/majority population distribution and returns lists of cut edges, minority seat wins, and tuples of minority percentage by district for each step of the chain.
    
    Parameters:
    graph (networkx.Graph) -- a NetworkX graph object representing the dual graph on which to run the chain. The nodes should have attributes for majority population, minority population, and total population.
    min_pop_col (string) -- the key/column name for the minority population attribute in graph
    maj_pop_col (string) -- the key/column name for the majority population attribute in graph
    tot_pop_col (string) -- the key/column name for the total population attribute in graph
    num_districts (int) -- number of districts to run for the chain
    initial_plan (gerrychain.Partition) -- an initial partition for the chain (which does not need updaters since the function will supply its own updaters)
    num_steps (int) -- the number of steps for which to run the chain
    pop_tol (float, default 0.05) -- tolerance for deviation from perfectly balanced populations between districts
    min_win_thresh (float, default 0.5) -- percent of minority population needed in a district for it to be considered a minority win. If the minority percentage in a district is greater than or equal to min_win_thresh then that district is considered a minority win.
    
    Returns:
    [cut_edges_list,min_seats_list,min_percents_list] (list)
        WHERE
        cut_edges_list (list) -- list where cut_edges_list[i] is the number of cut edges in the partition at step i of the Markov chain
        min_seats_list -- list where min_seats_list[i] is the number of districts won by the minority (according to min_win_thresh) at step i of the chain
        min_percents_list -- list where min_percents_list[i] is a tuple, with min_percents_list[i][j] being the minority percentage in district j at step i of the chain
    """
    my_updaters = {
        "population": Tally(tot_pop_col, alias = "population"),
        "cut_edges": cut_edges,
        "maj-min": Election("maj-min", {"maj": maj_pop_col, "min": min_pop_col}),
    }
    
    initial_partition = Partition(graph = initial_plan.graph, assignment = initial_plan.assignment, updaters = my_updaters)
    
    # ADD CONSTRAINTS
    popbound = within_percent_of_ideal_population(initial_partition, 0.1)
    
    # ########Setup Proposal
    ideal_population = sum(initial_partition["population"].values()) / len(initial_partition)
    
    tree_proposal = partial(
        recom,
        pop_col=tot_pop_col,
        pop_target=ideal_population,
        epsilon=pop_tol,
        node_repeats=1,
    )
    
    # ######BUILD MARKOV CHAINS
    
    recom_chain = MarkovChain(
        tree_proposal,
        Validator([popbound]),
        accept=always_accept,
        initial_state=initial_partition,
        total_steps=num_steps,
    )
    
    cut_edges_list = []
    min_seats_list = []
    min_percents_list = []
    
    for part in recom_chain:
        cut_edges_list.append(len(part["cut_edges"]))
        min_percents_list.append(part["maj-min"].percents("min"))
        min_seats = (np.array(part["maj-min"].percents("min")) >= min_win_thresh).sum()
        min_seats_list.append(min_seats)
    
    return [cut_edges_list,min_seats_list,min_percents_list]
示例#13
0
def main(config_data, id):
    """Runs a single experiment with the given config file. Loads a graph,
    runs a Chain to search for a Gerrymander, metamanders around that partition,
    runs another chain, and then saves the generated data.

    Args:
        config_data (Object): configuration of experiment loaded from JSON file
        id (String): id of experiment, used in tags to differentiate between
        experiments
    """
    try:

        timeBeg = time.time()
        print('Experiment', id, 'has begun')
        # Save configuration into global variable
        global config
        config = config_data

        # Get graph and dual
        graph, dual = preprocessing(config["INPUT_GRAPH_FILENAME"])
        # List of districts in original graph
        parts = list(set([graph.nodes[node][config['ASSIGN_COL']] for node in graph.nodes()]))
        # Ideal population of districts
        ideal_pop = sum([graph.nodes[node][config['POP_COL']] for node in graph.nodes()]) / len(parts)
        # Initialize partition
        election = Election(
                            config['ELECTION_NAME'],
                            {'PartyA': config['PARTY_A_COL'],
                            'PartyB': config['PARTY_B_COL']}
                            )

        updaters = {'population': Tally(config['POP_COL']),
                    'cut_edges': cut_edges,
                    config['ELECTION_NAME'] : election
                    }

        origPartition = Partition(graph=graph, assignment=config['ASSIGN_COL'], updaters=updaters)
        minAvg, minPartition = float('inf'), None
        for i in range(config['RUNS_PER_K_VAL']):
            tempGraph = copy.deepcopy(origPartition.graph)
            face_sierpinski_mesh(origPartition, tempGraph , getKFaces(dual, config['k']))
            # Refresh assignment and election of partition
            updaters[config['ELECTION_NAME']] = Election(
                                                         config['ELECTION_NAME'],
                                                         {'PartyA': config['PARTY_A_COL'],
                                                          'PartyB': config['PARTY_B_COL']}
                                                        )
            newPartition = Partition(graph=tempGraph, assignment=config['ASSIGN_COL'], updaters=updaters)
            if (avg := run_chain(newPartition, config['CHAIN_TYPE'],
                                               config['TEST_META_LENGTH'], ideal_pop, id + 'a' + str(i),
                                               config['TEST_RUN_STATS_TAG'] + id + str(i))) < minAvg:
                minAvg, minPartition = avg, newPartition

        updaters[config['ELECTION_NAME']] = Election(
                                                     config['ELECTION_NAME'],
                                                     {'PartyA': config['PARTY_A_COL'],
                                                      'PartyB': config['PARTY_B_COL']}
                                                    )
        partition = Partition(graph=minPartition.graph, assignment=config['ASSIGN_COL'], updaters=updaters)
        # Run chain again
        run_chain(partition, config['CHAIN_TYPE'], config['FULL_CHAIN_LENGTH'],
                  ideal_pop, id + 'b', config['FULL_RUN_STATS_TAG'] + id)
        # Save data from experiment to JSON files
        drawGraph(partition.graph, 'cut_times', config['GRAPH_TAG'] + '_single_raw_' + id)
        drawGraph(partition.graph, 'sibling_cuts', config['GRAPH_TAG'] + '_single_adjusted_' + id)
        drawDoubleGraph(partition.graph, 'cut_times', config['GRAPH_TAG'] + '_double_raw_' + id)
        drawDoubleGraph(partition.graph, 'sibling_cuts', config['GRAPH_TAG'] + '_double_adjusted_' + id)
        saveGraphStatistics(partition.graph, config['GRAPH_STATISTICS_TAG'] + id)

        print('Experiment {} completed in {:.2f} seconds'.format(id, time.time() - timeBeg))
def main(config_data, id):
    """Runs a single experiment with the given config file. Loads a graph,
    runs a Chain to search for a Gerrymander, metamanders around that partition,
    runs another chain, and then saves the generated data.

    Args:
        config_data (Object): configuration of experiment loaded from JSON file
        id (String): id of experiment, used in tags to differentiate between
        experiments
    """
    try:
        timeBeg = time.time()
        print('Experiment', id, 'has begun')
        # Save configuration into global variable
        global config
        config = config_data

        # Get graph and dual graph
        graph, dual = preprocessing(config["INPUT_GRAPH_FILENAME"])
        # List of districts in original graph
        parts = list(
            set([
                graph.nodes[node][config['ASSIGN_COL']]
                for node in graph.nodes()
            ]))
        # Ideal population of districts
        ideal_pop = sum(
            [graph.nodes[node][config['POP_COL']]
             for node in graph.nodes()]) / len(parts)
        # Initialize partition
        election = Election(config['ELECTION_NAME'], {
            'PartyA': config['PARTY_A_COL'],
            'PartyB': config['PARTY_B_COL']
        })

        updaters = {
            'population': Tally(config['POP_COL']),
            'cut_edges': cut_edges,
            config['ELECTION_NAME']: election
        }

        partition = Partition(graph=graph,
                              assignment=config['ASSIGN_COL'],
                              updaters=updaters)
        # Run Chain to search for a gerrymander, and get it
        mander = run_chain(partition, config['CHAIN_TYPE'],
                           config['FIND_GERRY_LENGTH'], ideal_pop, id + 'a',
                           config['ORIG_RUN_STATS_TAG'] + id)
        savePartition(mander, config['LEFT_MANDER_TAG'] + id)
        # Metamanders around the found gerrymander
        metamander_around_partition(mander, dual, config['TARGET_TAG'] + id,
                                    config['SECRET'], config['META_PARAM'])
        # Refresh assignment and election of partition
        updaters[config['ELECTION_NAME']] = Election(
            config['ELECTION_NAME'], {
                'PartyA': config['PARTY_A_COL'],
                'PartyB': config['PARTY_B_COL']
            })
        partition = Partition(graph=graph,
                              assignment=config['ASSIGN_COL'],
                              updaters=updaters)
        # Run chain again
        run_chain(partition, config['CHAIN_TYPE'],
                  config['SAMPLE_META_LENGTH'], ideal_pop, id + 'b',
                  config['GERRY_RUN_STATS_TAG'] + id)
        # Save data from experiment to JSON files
        drawGraph(partition.graph, 'cut_times',
                  config['GRAPH_TAG'] + '_single_raw_' + id)
        drawGraph(partition.graph, 'sibling_cuts',
                  config['GRAPH_TAG'] + '_single_adjusted_' + id)
        drawDoubleGraph(partition.graph, 'cut_times',
                        config['GRAPH_TAG'] + '_double_raw_' + id)
        drawDoubleGraph(partition.graph, 'sibling_cuts',
                        config['GRAPH_TAG'] + '_double_adjusted_' + id)
        saveGraphStatistics(partition.graph,
                            config['GRAPH_STATISTICS_TAG'] + id)

        print('Experiment {} completed in {:.2f} seconds'.format(
            id,
            time.time() - timeBeg))
    except Exception as e:
        # Print notification if any experiment fails to complete
        track = traceback.format_exc()
        print(track)
        print('Experiment {} failed to complete after {:.2f} seconds'.format(
            id,
            time.time() - timeBeg))