t4as

Software & Sims

Unity Gerrymandering Simulator

A Unity simulation of redistricting. It generates a map with Perlin noise and redraws districts with a local search that scores population equality, compactness and partisan outcome.

  • Unity
  • C#
  • Perlin noise
  • Local search
  • Parallel.ForEach
Unity Gerrymandering Simulator

What it is

A Unity simulation that shows how district boundaries change election results for the same population. It generates a map with population density and political leaning from layered Perlin noise, splits it into districts, and then redraws the districts with a local-search optimiser. Sliders set how much the optimiser cares about equal population, compact shapes and a target partisan outcome, and the metrics update live as the map changes.

Interface

Simulator UI with controls and live fairness metrics

Buttons start, stop and reset the run. Sliders set the weight of each metric in the score.

How it works

Redistricting. A greedy local search. Each iteration picks a random tile on a district border and tries moving it to the neighbouring district. The move is kept if the weighted score improves, otherwise it is reverted. Because only improving moves are kept, it can get stuck in local minima. Resetting with a new seed gives a different result.

Score terms (all minimised):

  • Population equality: the standard deviation of population across districts (lower is better).
  • Compactness: the mean squared distance of each tile from its district’s centre of mass (lower means more compact districts).
  • Partisan outcome: distance of the district results from a user-defined target.

Data. Everything is generated at runtime from Perlin noise, so no external dataset is needed. The model is illustrative, not based on real census data.

Code

Population density is a base noise layer plus two clamped, offset layers that create dense “city” clusters. Initial districts are assigned by nearest random seed point (worldBuilder.cs):

float population_density = 0;
population_density += Mathf.PerlinNoise((float)i / 10f, (float)k / 10f) * 0.4f;
population_density += Mathf.Clamp((Mathf.PerlinNoise((float)(i + 100) / 14f, (float)(k + 100) / 10f) * 4) - 2.7f, 0, 2);
population_density += Mathf.Clamp((Mathf.PerlinNoise((float)(i + 300) / 14f, (float)(k + 300) / 10f) * 4) - 3f, 0, 0.4f);

float f_side = Mathf.PerlinNoise((float)i / 10f, (float)k / 10f) - 0.51f;

int zone_id = assign_closest_point(new Vector2((float)i, (float)k), generated_random_points);

The compactness term is a sum over every tile, so it is computed with Parallel.ForEach and per-thread subtotals merged with Interlocked.Add:

float update_zone_data()
{
    // ... mean_of_population, vote_result_points, standard_deviation ...

    long distance_cost = 0;
    Parallel.ForEach(titles,
        () => 0,
        (j, loop, subtotal) =>
        {
            subtotal += (int)Mathf.Pow(get_distance(j.Position, centers_of_mass[j.zoneId, 0], centers_of_mass[j.zoneId, 1]), 2);
            return subtotal;
        },
        (finalResult) => Interlocked.Add(ref distance_cost, finalResult)
    );

    current_score_zonequality = (((float)distance_cost / 10000)) * zone_bonus;
    current_score_equality = standard_deviation * pop_bonus;
    current_score_votes = vote_result_points * 500 * vote_bonus;

    return current_score_equality + current_score_zonequality + current_score_votes;
}