def fillPopulationEnergyGraph(self, alignment):
        remainingObjects = self.children.copy()
        if alignment is Alignment.northSouth:
            blocksToActOn = self.westernChildBlocks
        else:
            blocksToActOn = self.northernChildBlocks

        # work west to east or north to south
        if len(blocksToActOn) > 0:
            for blockToActOn in blocksToActOn:
                blockToActOn.populationEnergy = blockToActOn.population
                remainingObjects.remove(blockToActOn)

            filledBlocks = blocksToActOn.copy()
            while len(remainingObjects) > 0:
                neighborsOfBlocks = getNeighborsForGraphObjectsInList(graphObjects=blocksToActOn,
                                                                      inList=remainingObjects)
                if len(neighborsOfBlocks) > 0:
                    blocksToActOn = neighborsOfBlocks
                else:
                    saveDataToFileWithDescription(data=self,
                                                  censusYear='',
                                                  stateName='',
                                                  descriptionOfInfo='ErrorCase-NoNeighborsForGraphGroups')
                    plotGraphObjectGroups([self.children, blocksToActOn], showDistrictNeighborConnections=True)
                    plotBlocksForRedistrictingGroup(self, showBlockNeighborConnections=True)
                    raise RuntimeError("Can't find neighbors for graph objects")

                blocksToActOnThisRound = blocksToActOn.copy()
                while len(blocksToActOnThisRound) > 0:
                    blocksActedUpon = []
                    for blockToActOn in blocksToActOnThisRound:
                        previousNeighbors = getNeighborsForGraphObjectsInList(graphObjects=[blockToActOn],
                                                                              inList=filledBlocks)
                        if len(previousNeighbors) is not 0:
                            lowestPopulationEnergyNeighbor = min(previousNeighbors,
                                                                 key=lambda block: block.populationEnergy)

                            blockToActOn.populationEnergy = lowestPopulationEnergyNeighbor.populationEnergy + blockToActOn.population
                            remainingObjects.remove(blockToActOn)
                            blocksActedUpon.append(blockToActOn)
                            filledBlocks.append(blockToActOn)
                    blocksToActOnThisRound = [block for block in blocksToActOnThisRound if block not in blocksActedUpon]
                    if len(blocksActedUpon) == 0:
                        saveDataToFileWithDescription(data=self,
                                                      censusYear='',
                                                      stateName='',
                                                      descriptionOfInfo='ErrorCase-BlocksCanNotFindPreviousNeighbor')
                        plotGraphObjectGroups([filledBlocks, blocksToActOnThisRound])
                        plotBlocksForRedistrictingGroup(self, showBlockNeighborConnections=True, showGraphHeatmap=True,
                                                        showBlockGraphIds=True)
                        raise ReferenceError("Can't find previous neighbor for {0}".format(blocksToActOnThisRound))

        # add population to surrounding neighbors population energy
        # this is to help create smoother graphs in urban areas
        for block in self.children:
            for neighborBlock in block.allNeighbors:
                neighborBlock.populationEnergy += block.population
    def children(self, children):
        if len(children) != len(set(children)):
            saveDataToFileWithDescription(data=self,
                                          censusYear='',
                                          stateName='',
                                          descriptionOfInfo='ErrorCase-DuplicateChildren')
            raise RuntimeError("Children contains duplicates: {0}".format(self))

        self.__children = children
        self.updateBlockContainerData()
    def getPopulationEnergySplit(self, alignment, shouldDrawGraph=False):
        polygonSplitResult = self.getPopulationEnergyPolygonSplit(alignment=alignment, shouldDrawGraph=shouldDrawGraph)
        polygonSplitResultType = polygonSplitResult[0]
        if polygonSplitResultType is SplitType.NoSplit:
            return SplitType.NoSplit, None
        elif polygonSplitResultType is SplitType.ForceSplitAllBlocks:
            return SplitType.ForceSplitAllBlocks, None

        polygonSplits = polygonSplitResult[1]

        aSplitPolygon = polygonSplits[0]
        bSplitPolygon = polygonSplits[1]

        if polygonSplitResultType is SplitType.SplitIncludedInSeam:
            seamOnEdge = True
            seamSplitPolygon = None
        else:
            seamOnEdge = False
            seamSplitPolygon = polygonSplitResult[2]

        aSplit = []
        bSplit = []
        seamSplit = []
        for block in self.children:
            if doesPolygonContainTheOther(container=aSplitPolygon, target=block.geometry, ignoreInteriors=False):
                aSplit.append(block)
            elif doesPolygonContainTheOther(container=bSplitPolygon, target=block.geometry, ignoreInteriors=False):
                bSplit.append(block)
            elif not seamOnEdge and doesPolygonContainTheOther(container=seamSplitPolygon, target=block.geometry,
                                                               ignoreInteriors=False):
                seamSplit.append(block)
            # elif allIntersectingPolygons(seamSplitPolygon, block.geometry):
            #     seamSplit.append(block)
            #     plotPolygons([block.geometry])
            else:
                saveDataToFileWithDescription(data=[self, alignment, aSplitPolygon, bSplitPolygon, seamSplitPolygon,
                                                    block.geometry, seamOnEdge, polygonSplitResultType],
                                              censusYear='',
                                              stateName='',
                                              descriptionOfInfo='ErrorCase-CouldNotFindContainerForBlock')
                plotPolygons([aSplitPolygon, bSplitPolygon, seamSplitPolygon, block.geometry])
                raise RuntimeError("Couldn't find a container for block: {0}".format(block.geometry))

        aSplitPopulation = sum(block.population for block in aSplit)
        bSplitPopulation = sum(block.population for block in bSplit)

        if aSplitPopulation < bSplitPopulation:
            aSplit += seamSplit
        else:
            bSplit += seamSplit

        if shouldDrawGraph:
            plotGraphObjectGroups(graphObjectGroups=[aSplit, bSplit])

        return SplitType.NormalSplit, (aSplit, bSplit)
def validateContiguousRedistrictingGroups(groupList):
    contiguousRegions = findContiguousGroupsOfGraphObjects(groupList)
    if len(contiguousRegions) > 1:
        saveDataToFileWithDescription(data=contiguousRegions,
                                      censusYear='',
                                      stateName='',
                                      descriptionOfInfo='ErrorCase-GroupsAreNotContiguous')
        plotGraphObjectGroups(contiguousRegions,
                              showDistrictNeighborConnections=True)
        raise ValueError("Don't have a contiguous set of RedistrictingGroups. There are {0} distinct groups".format(
            len(contiguousRegions)))
    def validateBlockNeighbors(self):
        contiguousRegions = findContiguousGroupsOfGraphObjects(self.children)
        if len(contiguousRegions) > 1:
            saveDataToFileWithDescription(data=[self, contiguousRegions],
                                          censusYear='',
                                          stateName='',
                                          descriptionOfInfo='ErrorCase-BlocksNotContiguous')
            plotGraphObjectGroups(contiguousRegions, showDistrictNeighborConnections=True)
            plotBlocksForRedistrictingGroup(self, showBlockNeighborConnections=True, showBlockGraphIds=True)
            raise RuntimeError("Don't have a contiguous set of AtomicBlocks. There are {0} distinct groups.".format(
                len(contiguousRegions)))

        for block in self.children:
            neighborBlocksNotInGroup = [neighborBlock for neighborBlock in block.allNeighbors
                                        if neighborBlock not in self.children]
            if len(neighborBlocksNotInGroup):
                saveDataToFileWithDescription(data=[self, block],
                                              censusYear='',
                                              stateName='',
                                              descriptionOfInfo='ErrorCase-BlockHasNeighborOutsideRedistrictingGroup')
                plotBlocksForRedistrictingGroup(self, showBlockNeighborConnections=True, showBlockGraphIds=True)
                raise RuntimeError("Some blocks have neighbor connections with block outside the redistricting group")
def splitLowestEnergySeam(candidateDistrictA, candidateDistrictB,
                          showDetailedProgress, energyRelativeToPopulation):
    groupsBetweenCandidates = getRedistrictingGroupsBetweenCandidates(
        candidateDistrictA, candidateDistrictB)
    groupBreakUpCandidates = [
        groupToBreakUp for groupToBreakUp in groupsBetweenCandidates
        if groupToBreakUp not in candidateDistrictA
    ]
    groupBreakUpCandidates = [
        groupBreakUpCandidate
        for groupBreakUpCandidate in groupBreakUpCandidates
        if len(groupBreakUpCandidate.children) > 1
        and groupBreakUpCandidate.population > 0
    ]
    seamsToEvaluate = []
    for groupBreakUpCandidate in groupBreakUpCandidates:
        westernAndEasternNeighbors = groupBreakUpCandidate.westernNeighbors + groupBreakUpCandidate.easternNeighbors
        if any([
                neighbor for neighbor in westernAndEasternNeighbors
                if neighbor in candidateDistrictA
        ]):
            seamsToEvaluate.append((groupBreakUpCandidate, Alignment.westEast))

        northernAndSouthernNeighbors = groupBreakUpCandidate.northernNeighbors + groupBreakUpCandidate.southernNeighbors
        if any([
                neighbor for neighbor in northernAndSouthernNeighbors
                if neighbor in candidateDistrictA
        ]):
            seamsToEvaluate.append(
                (groupBreakUpCandidate, Alignment.northSouth))
    tqdm.write(
        '      *** Finding lowest energy seam out of {0} seams ***'.format(
            len(seamsToEvaluate)))
    if showDetailedProgress:
        pbar = None
    else:
        pbar = tqdm(total=len(seamsToEvaluate))
    energyScores = []
    backupEnergyScores = []
    for seamToEvaluate in seamsToEvaluate:
        groupToEvaluate = seamToEvaluate[0]
        alignmentForEvaluation = seamToEvaluate[1]
        groupToEvaluate.fillPopulationEnergyGraph(alignmentForEvaluation)
        splitResult = groupToEvaluate.getPopulationEnergyPolygonSplit(
            alignmentForEvaluation)
        groupToEvaluate.clearPopulationEnergyGraph()
        polygonSplitResultType = splitResult[0]

        if polygonSplitResultType is SplitType.NoSplit:
            # if we can't split in this direction, we need to check the other direction
            # and if that direction can't be split, we'll call for a force split
            if alignmentForEvaluation is Alignment.northSouth:
                oppositeAlignment = Alignment.westEast
            else:
                oppositeAlignment = Alignment.northSouth

            groupToEvaluate.fillPopulationEnergyGraph(oppositeAlignment)
            oppositeSplitResult = groupToEvaluate.getPopulationEnergyPolygonSplit(
                oppositeAlignment)
            groupToEvaluate.clearPopulationEnergyGraph()
            oppositePolygonSplitResultType = oppositeSplitResult[0]
            if oppositePolygonSplitResultType is SplitType.NoSplit:
                seamEnergy = groupToEvaluate.population
                alignmentForEvaluation = Alignment.all
                energyScores.append((groupToEvaluate, alignmentForEvaluation,
                                     seamEnergy, polygonSplitResultType))

                if len(groupToEvaluate.children) >= 10:
                    tqdm.write(
                        "      *** Warning: Couldn't find a split for {0}. Candidate for Force Splitting. {1} blocks. {2} total pop."
                        .format(groupToEvaluate.graphId,
                                len(groupToEvaluate.children),
                                groupToEvaluate.population))
                    saveDataToFileWithDescription(
                        data=groupToEvaluate,
                        censusYear='',
                        stateName='',
                        descriptionOfInfo=
                        'WarningCase-ForceSplittingWithOver10Children-{0}'.
                        format(id(groupToEvaluate)))
            else:
                if oppositePolygonSplitResultType is SplitType.ForceSplitAllBlocks:
                    # will need to remove any other seams in list if we ever take
                    # more than the first seam in the sorted list below
                    seamEnergy = groupToEvaluate.population
                    alignmentForEvaluation = Alignment.all
                else:
                    seamEnergy = oppositeSplitResult[3]
                    alignmentForEvaluation = oppositeAlignment
                backupEnergyScores.append(
                    (groupToEvaluate, alignmentForEvaluation, seamEnergy,
                     oppositePolygonSplitResultType))
        else:
            if polygonSplitResultType is SplitType.ForceSplitAllBlocks:
                # will need to remove any other seams in list if we ever take
                # more than the first seam in the sorted list below
                seamEnergy = groupToEvaluate.population
                alignmentForEvaluation = Alignment.all
            else:
                seamEnergy = splitResult[3]
            energyScores.append((groupToEvaluate, alignmentForEvaluation,
                                 seamEnergy, polygonSplitResultType))
        if pbar is not None:
            pbar.update(1)
    if pbar is not None:
        pbar.close()
    if len(energyScores) == 0:
        tqdm.write(
            "      *** Warning: Did not find any energy scores in this list: {0}"
            .format([group.graphId for group in groupBreakUpCandidates]))
        tqdm.write("          Switching to backup scores: {0} ***".format(
            backupEnergyScores))
        energyScores = backupEnergyScores

    if energyRelativeToPopulation:
        relativeEnergyScores = []
        for energyScore in energyScores:
            scoreGroup = energyScore[0]
            scoreAlignment = energyScore[1]
            currentScore = energyScore[2]
            scoreSplitResultType = energyScore[3]
            newScore = currentScore / scoreGroup.population
            relativeEnergyScores.append(
                (scoreGroup, scoreAlignment, newScore, scoreSplitResultType))
        energyScores = relativeEnergyScores

    energyScores.sort(key=lambda x: x[2])
    minimumEnergySeam = energyScores[0]
    groupToBreakUp = minimumEnergySeam[0]
    groupToBreakUpSeamAlignment = minimumEnergySeam[1]
    groupsToBreakUp = [(groupToBreakUp, groupToBreakUpSeamAlignment)]
    return groupsToBreakUp
    def splitDistrict(self,
                      numberOfDistricts,
                      populationDeviation,
                      weightingMethod,
                      breakingMethod,
                      totalSplitCount=None,
                      shouldMergeIntoFormerRedistrictingGroups=False,
                      shouldRefillEachPass=False,
                      shouldDrawFillAttempts=False,
                      shouldDrawEachStep=False,
                      fastCalculations=True,
                      showDetailedProgress=False,
                      shouldSaveProgress=True):
        if totalSplitCount is None:
            tqdm.write('*** Splitting into {0} districts ***'.format(
                numberOfDistricts))
            totalSplitCount = 0

        districts = []

        if numberOfDistricts == 1:
            return [self]

        aRatio = math.floor(numberOfDistricts / 2)
        bRatio = math.ceil(numberOfDistricts / 2)
        ratio = (aRatio, bRatio)

        districtSplitScores = []
        thisSplitCount = 0
        originalBreakingMethod = breakingMethod
        fillOriginDirection = None
        doneFindingSplits = False
        while not doneFindingSplits:
            tqdm.write('   *** Split starting. Using {0} and {1} ***'.format(
                weightingMethod, breakingMethod))

            cutDistrictInfo = self.cutDistrictIntoExactRatio(
                ratio=ratio,
                populationDeviation=populationDeviation,
                weightingMethod=weightingMethod,
                breakingMethod=breakingMethod,
                fillOriginDirection=fillOriginDirection,
                shouldDrawFillAttempts=shouldDrawFillAttempts,
                shouldDrawEachStep=shouldDrawEachStep,
                shouldMergeIntoFormerRedistrictingGroups=
                shouldMergeIntoFormerRedistrictingGroups,
                shouldRefillEachPass=shouldRefillEachPass,
                fastCalculations=fastCalculations,
                showDetailedProgress=showDetailedProgress,
                shouldSaveProgress=shouldSaveProgress)
            cutDistrict = cutDistrictInfo[0]
            fillOriginDirection = cutDistrictInfo[1]
            thisSplitCount += 1
            if cutDistrict is None:
                tqdm.write(
                    '   *** Failed to cut district. Attempt #:{0} for {1} ***'.
                    format(thisSplitCount, id(self)))
                districtSplitScores.append((None, 0, fillOriginDirection, 0))
            else:
                tqdm.write(
                    '   *** Cut district into exact ratio. Attempt #:{0} for {1} ***'
                    .format(thisSplitCount, id(self)))

                aDistrictCandidate = District(childrenGroups=cutDistrict[0])
                bDistrictCandidate = District(childrenGroups=cutDistrict[1])
                isAGood = isPolygonAGoodDistrictShape(
                    districtPolygon=aDistrictCandidate.geometry,
                    parentPolygon=self.geometry)
                isBGood = isPolygonAGoodDistrictShape(
                    districtPolygon=bDistrictCandidate.geometry,
                    parentPolygon=self.geometry)
                aPolsbyPopperScore = polsbyPopperScoreOfPolygon(
                    aDistrictCandidate.geometry)
                bPolsbyPopperScore = polsbyPopperScoreOfPolygon(
                    bDistrictCandidate.geometry)
                minimumPolsbyPopperScore = min(aPolsbyPopperScore,
                                               bPolsbyPopperScore)
                splitScore = 0
                if isAGood:
                    splitScore += 1
                if isBGood:
                    splitScore += 1

                saveDescription = 'SplitCandidate-{0}-{1}-{2}-'.format(
                    id(self), fillOriginDirection, breakingMethod)
                saveDataToFileWithDescription(
                    data=[aDistrictCandidate, bDistrictCandidate],
                    censusYear='',
                    stateName='',
                    descriptionOfInfo=saveDescription)
                saveGeometry = (saveDescription, aDistrictCandidate.geometry,
                                bDistrictCandidate.geometry, self.geometry)
                aDistrictCandidate = None
                bDistrictCandidate = None
                gc.collect()

                districtSplitScores.append({
                    'saveDescription': saveDescription,
                    'splitScore': splitScore,
                    'fillOriginDirection': fillOriginDirection,
                    'minimumPolsbyPopperScore': minimumPolsbyPopperScore,
                    'breakingMethod': breakingMethod,
                    'geometryInfo': saveGeometry
                })

                if splitScore is 2:
                    doneFindingSplits = True
                else:
                    tqdm.write(
                        '   *** One or more split candidates is not a good shape! Trying again. ***'
                    )
                    if shouldMergeIntoFormerRedistrictingGroups:
                        tqdm.write(
                            '      *** Merging district into starting groups ***'
                        )
                        redistrictingGroupsInDistrict = cutDistrict[
                            0] + cutDistrict[1]
                        mergedRedistrictingGroups = mergeCandidatesIntoPreviousGroups(
                            candidates=[redistrictingGroupsInDistrict])[0]
                        tqdm.write(
                            '      *** Re-attaching new Redistricting Groups to existing Groups ***'
                        )
                        assignNeighboringRedistrictingGroupsToRedistrictingGroups(
                            changedRedistrictingGroups=
                            mergedRedistrictingGroups,
                            allNeighborCandidates=mergedRedistrictingGroups)
                        validateRedistrictingGroups(mergedRedistrictingGroups)
                        self.children = mergedRedistrictingGroups

            splitScoresWithCurrentBreakingMethod = [
                districtSplitScore
                for districtSplitScore in districtSplitScores
                if districtSplitScore['breakingMethod'] is breakingMethod
            ]
            fillOriginDirection = getOppositeDirection(fillOriginDirection)
            directionsTried = [
                districtSplitScore['fillOriginDirection']
                for districtSplitScore in splitScoresWithCurrentBreakingMethod
            ]
            if fillOriginDirection in directionsTried:
                fillOriginDirection = getCWDirection(fillOriginDirection)
                if fillOriginDirection in directionsTried:
                    fillOriginDirection = getOppositeDirection(
                        fillOriginDirection)
                    if fillOriginDirection in directionsTried:
                        if breakingMethod is BreakingMethod.splitLowestRelativeEnergySeam or breakingMethod is BreakingMethod.splitLowestEnergySeam:
                            breakingMethod = BreakingMethod.splitGroupsOnEdge
                        else:
                            doneFindingSplits = True

        districtSplitScores.sort(key=lambda x: x['minimumPolsbyPopperScore'],
                                 reverse=True)
        saveDataToFileWithDescription(
            data=districtSplitScores,
            censusYear='',
            stateName='',
            descriptionOfInfo='DistrictSplitScores-{0}'.format(id(self)))
        bestDistrictSplitInfo = districtSplitScores[0]
        bestDistrictSaveDescription = bestDistrictSplitInfo['saveDescription']
        if bestDistrictSaveDescription is None:
            plotDistrict(self, showDistrictNeighborConnections=True)
            raise RuntimeError(
                "Could not find a good split for {0} from list: {1}".format(
                    id(self), districtSplitScores))
        bestDistrictSplit = loadDataFromFileWithDescription(
            censusYear='',
            stateName='',
            descriptionOfInfo=bestDistrictSaveDescription)
        aDistrict = bestDistrictSplit[0]
        bDistrict = bestDistrictSplit[1]
        bestSplitScore = bestDistrictSplitInfo['splitScore']
        bestFillDirection = bestDistrictSplitInfo['fillOriginDirection']
        bestPolsbyPopperScore = bestDistrictSplitInfo[
            'minimumPolsbyPopperScore']
        bestBreakingMethod = bestDistrictSplitInfo['breakingMethod']
        tqdm.write(
            '   *** Chose a district split! Ratio: {0}***'.format(ratio))
        tqdm.write(
            '   *** Direction: {0} Split score: {1} Polsby-Popper score: {2} Breaking method: {3} ***'
            .format(bestFillDirection, bestSplitScore, bestPolsbyPopperScore,
                    bestBreakingMethod))
        totalSplitCount += 1

        aDistrictSplits = aDistrict.splitDistrict(
            numberOfDistricts=aRatio,
            populationDeviation=populationDeviation,
            weightingMethod=weightingMethod,
            breakingMethod=originalBreakingMethod,
            totalSplitCount=totalSplitCount,
            shouldMergeIntoFormerRedistrictingGroups=
            shouldMergeIntoFormerRedistrictingGroups,
            shouldRefillEachPass=shouldRefillEachPass,
            shouldDrawFillAttempts=shouldDrawFillAttempts,
            shouldDrawEachStep=shouldDrawEachStep,
            fastCalculations=fastCalculations,
            showDetailedProgress=showDetailedProgress,
            shouldSaveProgress=shouldSaveProgress)
        districts.extend(aDistrictSplits)

        bDistrictSplits = bDistrict.splitDistrict(
            numberOfDistricts=bRatio,
            populationDeviation=populationDeviation,
            weightingMethod=weightingMethod,
            breakingMethod=originalBreakingMethod,
            totalSplitCount=totalSplitCount,
            shouldMergeIntoFormerRedistrictingGroups=
            shouldMergeIntoFormerRedistrictingGroups,
            shouldRefillEachPass=shouldRefillEachPass,
            shouldDrawFillAttempts=shouldDrawFillAttempts,
            shouldDrawEachStep=shouldDrawEachStep,
            fastCalculations=fastCalculations,
            showDetailedProgress=showDetailedProgress,
            shouldSaveProgress=shouldSaveProgress)
        districts.extend(bDistrictSplits)

        return districts
    def cutDistrictIntoExactRatio(
            self,
            ratio,
            populationDeviation,
            weightingMethod,
            breakingMethod,
            fillOriginDirection=None,
            shouldDrawFillAttempts=False,
            shouldDrawEachStep=False,
            shouldMergeIntoFormerRedistrictingGroups=False,
            shouldRefillEachPass=False,
            fastCalculations=True,
            showDetailedProgress=False,
            shouldSaveProgress=True):

        ratioTotal = ratio[0] + ratio[1]
        idealDistrictASize = int(self.population / (ratioTotal / ratio[0]))
        idealDistrictBSize = int(self.population / (ratioTotal / ratio[1]))
        candidateDistrictA = []
        candidateDistrictB = []
        districtStillNotExactlyCut = True
        tqdm.write(
            '   *** Attempting forest fire fill for a {0} to {1} ratio on: ***'
            .format(ratio[0], ratio[1], id(self)))

        districtAStartingGroup = None
        count = 1
        while districtStillNotExactlyCut:
            tqdm.write(
                '      *** Starting forest fire fill pass #{0} ***'.format(
                    count))

            if districtAStartingGroup is None:
                if len(candidateDistrictA) == 0:
                    districtAStartingGroup = None
                else:
                    if shouldRefillEachPass:
                        districtAStartingGroup = None
                    else:
                        districtAStartingGroup = candidateDistrictA

            if breakingMethod is BreakingMethod.splitBestCandidateGroup:
                returnBestCandidateGroup = True
            else:
                returnBestCandidateGroup = False

            districtCandidateResult = self.cutDistrictIntoRoughRatio(
                idealDistrictASize=idealDistrictASize,
                weightingMethod=weightingMethod,
                districtAStartingGroup=districtAStartingGroup,
                fillOriginDirection=fillOriginDirection,
                shouldDrawEachStep=shouldDrawEachStep,
                returnBestCandidateGroup=returnBestCandidateGroup,
                fastCalculations=fastCalculations)
            districtCandidates = districtCandidateResult[0]
            nextBestGroupForCandidateDistrictA = districtCandidateResult[1]
            fillOriginDirection = districtCandidateResult[2]
            districtAStartingGroup = districtCandidateResult[3]

            candidateDistrictA = districtCandidates[0]
            candidateDistrictB = districtCandidates[1]

            if shouldDrawFillAttempts:
                if nextBestGroupForCandidateDistrictA is None:
                    nextBestGroupForCandidateDistrictA = []
                plotGraphObjectGroups(
                    graphObjectGroups=[
                        candidateDistrictA, candidateDistrictB,
                        nextBestGroupForCandidateDistrictA
                    ],
                    showDistrictNeighborConnections=True,
                    saveImages=True,
                    saveDescription='DistrictSplittingIteration-{0}-{1}'.
                    format(id(self), count))

            candidateDistrictAPop = sum(group.population
                                        for group in candidateDistrictA)
            candidateDistrictBPop = sum(group.population
                                        for group in candidateDistrictB)

            if idealDistrictASize - populationDeviation <= candidateDistrictAPop <= idealDistrictASize + populationDeviation and \
                    idealDistrictBSize - populationDeviation <= candidateDistrictBPop <= idealDistrictBSize + populationDeviation:
                districtStillNotExactlyCut = False
            else:
                tqdm.write(
                    '      *** Unsuccessful fill attempt. {0} off the count. ***'
                    .format(abs(idealDistrictASize - candidateDistrictAPop)))
                if len(self.children) == 1:
                    # this means that the candidate couldn't fill because there a single redistricting group
                    # likely because there was a single county
                    groupsToBreakUp = [(self.children[0], Alignment.all)]
                elif len(candidateDistrictA) == 0:
                    # we didn't get anything in candidateA, which means none of the children met the conditions
                    # so, we won't get anything to break up, let's break the first starting candidate with instead
                    breakupCandidates = [
                        startingCandidateTuple[0] for startingCandidateTuple in
                        self.getCutStartingCandidates()
                    ]
                    breakupCandidates = [
                        breakupCandidate
                        for breakupCandidate in breakupCandidates
                        if len(breakupCandidate.children) > 1
                    ]
                    if len(breakupCandidates) == 0:
                        saveDataToFileWithDescription(
                            data=[
                                self, districtAStartingGroup, ratio,
                                candidateDistrictA, candidateDistrictB,
                                nextBestGroupForCandidateDistrictA,
                                breakupCandidates
                            ],
                            censusYear='',
                            stateName='',
                            descriptionOfInfo=
                            'ErrorCase-NoGroupsCandidatesCapableOfBreaking')

                        tqdm.write(
                            '   *** Failed fill attempt!!! *** <------------------------------------------------------'
                        )
                        tqdm.write(
                            "Couldn't fill and all breakup candidates have too few children!!!! For {0}"
                            .format(id(self)))
                        return None, fillOriginDirection
                    groupsToBreakUp = [(breakupCandidates[0], Alignment.all)]
                else:
                    if len(candidateDistrictB) == 1:
                        groupsToBreakUp = [(candidateDistrictB[0],
                                            Alignment.all)]
                    else:
                        if breakingMethod is BreakingMethod.splitBestCandidateGroup:
                            groupsToBreakUp = [
                                (nextBest, Alignment.all) for nextBest in
                                nextBestGroupForCandidateDistrictA
                            ]
                        elif breakingMethod is BreakingMethod.splitGroupsOnEdge:
                            groupsToBreakUp = splitGroupsOnEdge(
                                candidateDistrictA, candidateDistrictB,
                                shouldMergeIntoFormerRedistrictingGroups,
                                shouldRefillEachPass)
                        elif breakingMethod is BreakingMethod.splitLowestEnergySeam:
                            groupsToBreakUp = splitLowestEnergySeam(
                                candidateDistrictA,
                                candidateDistrictB,
                                showDetailedProgress,
                                energyRelativeToPopulation=False)
                        elif breakingMethod is BreakingMethod.splitLowestRelativeEnergySeam:
                            groupsToBreakUp = splitLowestEnergySeam(
                                candidateDistrictA,
                                candidateDistrictB,
                                showDetailedProgress,
                                energyRelativeToPopulation=True)
                        else:
                            raise RuntimeError(
                                '{0} is not supported'.format(breakingMethod))

                groupsCapableOfBreaking = [
                    groupToBreakUp for groupToBreakUp in groupsToBreakUp
                    if len(groupToBreakUp[0].children) > 1
                ]
                if len(groupsCapableOfBreaking) == 0:
                    saveDataToFileWithDescription(
                        data=[
                            self, districtAStartingGroup, ratio,
                            candidateDistrictA, candidateDistrictB,
                            nextBestGroupForCandidateDistrictA
                        ],
                        censusYear='',
                        stateName='',
                        descriptionOfInfo='ErrorCase-NoGroupsCapableOfBreaking'
                    )
                    plotGraphObjectGroups(
                        [self.children, districtAStartingGroup])
                    raise RuntimeError(
                        "Groups to break up don't meet criteria. Groups: {0}".
                        format([
                            groupToBreakUp[0].graphId
                            for groupToBreakUp in groupsToBreakUp
                        ]))

                tqdm.write(
                    '      *** Graph splitting {0} redistricting groups ***'.
                    format(len(groupsCapableOfBreaking)))
                updatedChildren = self.children.copy()
                newRedistrictingGroups = []
                if showDetailedProgress:
                    pbar = None
                else:
                    pbar = tqdm(total=len(groupsCapableOfBreaking))
                for groupToBreakUpItem in groupsCapableOfBreaking:
                    if showDetailedProgress:
                        countForProgress = groupsCapableOfBreaking.index(
                            groupToBreakUpItem) + 1
                    else:
                        countForProgress = None
                    groupToBreakUp = groupToBreakUpItem[0]
                    alignmentForSplits = groupToBreakUpItem[1]
                    smallerRedistrictingGroups = groupToBreakUp.getGraphSplits(
                        shouldDrawGraph=shouldDrawEachStep,
                        alignment=alignmentForSplits,
                        countForProgress=countForProgress)
                    updatedChildren.extend(smallerRedistrictingGroups)
                    updatedChildren.remove(groupToBreakUp)

                    # assign the previous parent graphId so that we can combine the parts again after the exact split
                    for smallerRedistrictingGroup in smallerRedistrictingGroups:
                        if groupToBreakUp.previousParentId is None:
                            previousParentId = groupToBreakUp.graphId
                        else:
                            previousParentId = groupToBreakUp.previousParentId
                        smallerRedistrictingGroup.previousParentId = previousParentId

                    newRedistrictingGroups.extend(smallerRedistrictingGroups)
                    if pbar is not None:
                        pbar.update(1)
                if pbar is not None:
                    pbar.close()

                tqdm.write(
                    '      *** Re-attaching new Redistricting Groups to existing Groups ***'
                )
                assignNeighboringRedistrictingGroupsToRedistrictingGroups(
                    changedRedistrictingGroups=newRedistrictingGroups,
                    allNeighborCandidates=updatedChildren)
                validateRedistrictingGroups(updatedChildren)

                tqdm.write('      *** Updating District Candidate Data ***')
                self.children = updatedChildren

                # need to make sure the starting group still is in the district
                if districtAStartingGroup not in self.children:
                    districtAStartingGroup = None

            shouldSaveThisPass = True
            if breakingMethod is BreakingMethod.splitLowestEnergySeam:
                if count % 10 != 0:
                    shouldSaveThisPass = False

            if shouldSaveProgress:
                if shouldSaveThisPass:
                    saveDataToFileWithDescription(
                        data=(self, candidateDistrictA, ratio,
                              fillOriginDirection),
                        censusYear='',
                        stateName='',
                        descriptionOfInfo='DistrictSplitLastIteration-{0}'.
                        format(id(self)))
            count += 1

        if shouldMergeIntoFormerRedistrictingGroups:
            tqdm.write(
                '      *** Merging candidates into remaining starting groups ***'
            )
            mergedCandidates = mergeCandidatesIntoPreviousGroups(
                candidates=[candidateDistrictA, candidateDistrictB])
            candidateDistrictA = mergedCandidates[0]
            candidateDistrictB = mergedCandidates[1]
            tqdm.write(
                '      *** Re-attaching new Redistricting Groups to existing Groups ***'
            )
            assignNeighboringRedistrictingGroupsToRedistrictingGroups(
                changedRedistrictingGroups=candidateDistrictA,
                allNeighborCandidates=candidateDistrictA)
            assignNeighboringRedistrictingGroupsToRedistrictingGroups(
                changedRedistrictingGroups=candidateDistrictB,
                allNeighborCandidates=candidateDistrictB)
            validateRedistrictingGroups(candidateDistrictA)
            validateRedistrictingGroups(candidateDistrictB)

        tqdm.write(
            '   *** Successful fill attempt!!! *** <------------------------------------------------------------'
        )
        return (candidateDistrictA, candidateDistrictB), fillOriginDirection
    def getPopulationEnergyPolygonSplit(self, alignment, shouldDrawGraph=False):
        finishingBlocksToAvoid = []
        while True:
            lowestEnergySeamResult = self.getLowestPopulationEnergySeam(alignment=alignment,
                                                                        finishingBlocksToAvoid=finishingBlocksToAvoid)
            if lowestEnergySeamResult is None:
                return SplitType.NoSplit, None
            lowestEnergySeam = lowestEnergySeamResult[0]
            energySeamFinishingBlock = lowestEnergySeamResult[1]
            energySeamStartingEnergy = lowestEnergySeamResult[2]

            seamSplitPolygon = polygonFromMultipleGeometries(geometryList=lowestEnergySeam)
            polygonWithoutSeam = self.geometry.difference(seamSplitPolygon)

            # if the polygon without the seam is empty, that means we have a small enough redistricting group where
            # we need to break it up completely. Because our seams can no longer break up any further.
            if polygonWithoutSeam.is_empty:
                return SplitType.ForceSplitAllBlocks, None

            if type(polygonWithoutSeam) is MultiPolygon:
                seamOnEdge = False
                splitPolygons = list(polygonWithoutSeam)
            else:
                seamOnEdge = True
                splitPolygons = [polygonWithoutSeam, seamSplitPolygon]

            if alignment is Alignment.northSouth:
                aSplitRepresentativeBlockDirection = CardinalDirection.north
                bSplitRepresentativeBlockDirection = CardinalDirection.south
            else:
                aSplitRepresentativeBlockDirection = CardinalDirection.west
                bSplitRepresentativeBlockDirection = CardinalDirection.east

            # Identify which polygon is in which direction
            # Note: Need to make sure we don't select a block in the seam so we supply a list without those blocks
            #   If the seam is completely on the edge though, let's include the seam
            if seamOnEdge:
                borderChildrenRepresentativeCandidates = self.borderChildren
            else:
                borderChildrenRepresentativeCandidates = [child for child in self.borderChildren if
                                                          child not in lowestEnergySeam]

            if len(borderChildrenRepresentativeCandidates) == 0:
                return SplitType.NoSplit, None

            aSplitRepresentativeBlock = mostCardinalOfGeometries(geometryList=borderChildrenRepresentativeCandidates,
                                                                 direction=aSplitRepresentativeBlockDirection)

            bSplitRepresentativeBlock = mostCardinalOfGeometries(geometryList=borderChildrenRepresentativeCandidates,
                                                                 direction=bSplitRepresentativeBlockDirection)

            aSplitPolygon = getPolygonThatContainsGeometry(polygonList=splitPolygons,
                                                           targetGeometry=aSplitRepresentativeBlock,
                                                           useTargetRepresentativePoint=True)
            bSplitPolygon = getPolygonThatContainsGeometry(polygonList=splitPolygons,
                                                           targetGeometry=bSplitRepresentativeBlock,
                                                           useTargetRepresentativePoint=True)
            leftOverPolygons = [geometry for geometry in splitPolygons if
                                geometry is not aSplitPolygon and geometry is not bSplitPolygon]
            if aSplitPolygon is None or bSplitPolygon is None:
                plotPolygons(splitPolygons + [aSplitRepresentativeBlock.geometry, bSplitRepresentativeBlock.geometry])
                saveDataToFileWithDescription(data=self,
                                              censusYear='',
                                              stateName='',
                                              descriptionOfInfo='ErrorCase-AorBSplitIsNone')
                raise RuntimeError('Split a or b not found')

            if aSplitPolygon is bSplitPolygon:
                finishingBlocksToAvoid.append(energySeamFinishingBlock)
                continue

            if len(leftOverPolygons) is not len(splitPolygons) - 2:
                saveDataToFileWithDescription(data=self,
                                              censusYear='',
                                              stateName='',
                                              descriptionOfInfo='ErrorCase-MissingPolygons')
                raise RuntimeError('Missing some polygons for mapping. Split polygons: {0} Left over polygon: {1}'
                                   .format(len(splitPolygons), len(leftOverPolygons)))

            polygonSplits = (aSplitPolygon, bSplitPolygon)

            if shouldDrawGraph:
                plotPolygons(polygonSplits)

            if seamOnEdge:
                return SplitType.SplitIncludedInSeam, polygonSplits, None, energySeamStartingEnergy
            else:
                seamSplitPolygon = polygonFromMultiplePolygons(polygonList=[seamSplitPolygon] + leftOverPolygons)
                return SplitType.NormalSplit, polygonSplits, seamSplitPolygon, energySeamStartingEnergy
    districtGeometries = EsriDumper(
        url='https://tigerweb.geo.census.gov/arcgis/rest/services/Generalized_ACS2017/Legislative/MapServer/5',
        extra_query_args={'where': 'STATE=\'{0}\''.format(stateFIPSCode)})
    # https://github.com/openaddresses/pyesridump

    existingDistricts = []
    for districtGeometry in districtGeometries:
        geoJSONGeometry = districtGeometry['geometry']
        districtNumber = districtGeometry['properties']['BASENAME']
        existingDistrict = ExistingDistrict(districtNumber=districtNumber, geoJSONGeometry=geoJSONGeometry)
        existingDistricts.append(existingDistrict)

    return existingDistricts


stateAbbreviation = 'MI'
stateInfo = states.lookup(stateAbbreviation)
censusYear = 2010
descriptionToWorkWith = 'All'

allCongressionalDistrictGeosInState = getAllGeoDataForFederalCongressionalDistricts(stateFIPSCode=stateInfo.fips)
# save county data to file
saveDataToFileWithDescription(data=allCongressionalDistrictGeosInState,
                              censusYear=censusYear,
                              stateName=stateInfo.name,
                              descriptionOfInfo='{0}CurrentFederalCongressionalDistricts'.format(descriptionToWorkWith))
saveGeoJSONToDirectoryWithDescription(geographyList=allCongressionalDistrictGeosInState,
                                      censusYear=censusYear,
                                      stateName=stateInfo.name,
                                      descriptionOfInfo='CurrentFederalCongressionalDistricts')
Example #11
0
stateAbbreviation = 'MI'
stateInfo = states.lookup(stateAbbreviation)
censusYear = 2010
descriptionToWorkWith = 'All'

censusData = loadDataFromFileWithDescription(
    censusYear=censusYear,
    stateName=stateInfo.name,
    descriptionOfInfo='{0}Block'.format(descriptionToWorkWith))
redistrictingGroupList = createRedistrictingGroupsWithAtomicBlocksFromCensusData(
    censusData=censusData)
# exportGeographiesToShapefile(geographyList=AtomicBlock.atomicBlockList, descriptionOfInfo='AtomicGroups')
saveDataToFileWithDescription(
    data=redistrictingGroupList,
    censusYear=censusYear,
    stateName=stateInfo.name,
    descriptionOfInfo='{0}RedistrictingGroupPreGraph'.format(
        descriptionToWorkWith))

redistrictingGroupList = prepareBlockGraphsForRedistrictingGroups(
    redistrictingGroupList)
saveDataToFileWithDescription(
    data=redistrictingGroupList,
    censusYear=censusYear,
    stateName=stateInfo.name,
    descriptionOfInfo='{0}RedistrictingGroupBlockGraphsPrepared'.format(
        descriptionToWorkWith))

redistrictingGroupList = prepareGraphsForRedistrictingGroups(
    redistrictingGroupList)
saveDataToFileWithDescription(
    else:
        return None


stateAbbreviation = 'MI'
stateInfo = states.lookup(stateAbbreviation)
censusYear = 2010
descriptionToWorkWith = 'All'

censusRequest = Census(apiKeys.censusAPIKey, year=censusYear)
countyInfoList = getCountiesInState(stateFIPSCode=stateInfo.fips,
                                    maxNumberOfCounties=math.inf)
allCountyGeosInState = allGeoDataForEachCounty(
    existingCountyData=countyInfoList)
# save county data to file
saveDataToFileWithDescription(
    data=allCountyGeosInState,
    censusYear=censusYear,
    stateName=stateInfo.name,
    descriptionOfInfo='{0}County'.format(descriptionToWorkWith))

allBlocksInState = getAllBlocksInState(countyList=countyInfoList)
allBlockGeosInState = allGeoDataForEachBlock(
    countyInfoList=countyInfoList, existingBlockData=allBlocksInState)
# save block data to file
saveDataToFileWithDescription(
    data=allBlockGeosInState,
    censusYear=censusYear,
    stateName=stateInfo.name,
    descriptionOfInfo='{0}Block'.format(descriptionToWorkWith))
                                                          descriptionToWorkWith))

initialDistrict = createDistrictFromRedistrictingGroups(redistrictingGroups=redistrictingGroups)

populationDeviation = populationDeviationFromPercent(overallPercentage=overallPercentageOffIdealAllowed,
                                                     numberOfDistricts=numberOfDistricts,
                                                     totalPopulation=initialDistrict.population)

districts = initialDistrict.splitDistrict(numberOfDistricts=numberOfDistricts,
                                          populationDeviation=populationDeviation,
                                          weightingMethod=WeightingMethod.cardinalDistance,
                                          breakingMethod=BreakingMethod.splitGroupsOnEdge,
                                          shouldMergeIntoFormerRedistrictingGroups=True,
                                          shouldDrawEachStep=False,
                                          shouldRefillEachPass=True,
                                          fastCalculations=False,
                                          showDetailedProgress=False)
saveDataToFileWithDescription(data=districts,
                              censusYear=censusYear,
                              stateName=stateInfo,
                              descriptionOfInfo='{0}-FederalDistricts'.format(descriptionToWorkWith))
saveGeoJSONToDirectoryWithDescription(geographyList=districts,
                                      censusYear=censusYear,
                                      stateName=stateInfo,
                                      descriptionOfInfo='FederalDistrictsGeoJSON')
plotDistricts(districts=districts,
              showPopulationCounts=False,
              showDistrictNeighborConnections=False)
districtPolygons = [district.geometry for district in districts]
plotPolygons(districtPolygons)