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 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")
Пример #5
0
    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 getLowestPopulationEnergySeam(self, alignment, shouldDrawGraph=False, finishingBlocksToAvoid=None):
        if alignment is Alignment.northSouth:
            startingCandidates = self.easternChildBlocks
            borderBlocksToAvoid = self.northernChildBlocks + self.southernChildBlocks + startingCandidates
            finishCandidates = self.westernChildBlocks
        else:
            startingCandidates = self.southernChildBlocks
            borderBlocksToAvoid = self.westernChildBlocks + self.easternChildBlocks + startingCandidates
            finishCandidates = self.northernChildBlocks

        if finishingBlocksToAvoid:
            finishCandidates = [candidate for candidate in finishCandidates if candidate not in finishingBlocksToAvoid]

        if len(startingCandidates) == 0 or len(finishCandidates) == 0:
            return None

        startingBlock = min(startingCandidates, key=lambda block: block.populationEnergy)
        startingBlockEnergy = startingBlock.populationEnergy
        blockToActOn = startingBlock

        lowestPopulationEnergySeam = [blockToActOn]
        failedStartingBlocks = []
        avoidingAdjacentBorderBlocks = True
        finishedSeam = False
        finishingBlock = None
        count = 1
        while not finishedSeam:
            if alignment is Alignment.northSouth:
                primaryNeighborCandidates = blockToActOn.westernNeighbors
            else:
                primaryNeighborCandidates = blockToActOn.northernNeighbors
            neighborCandidates = [block for block in primaryNeighborCandidates
                                  if block not in lowestPopulationEnergySeam and
                                  block not in borderBlocksToAvoid]

            if len(neighborCandidates) is 0:
                neighborCandidates = [block for block in blockToActOn.allNeighbors
                                      if block not in lowestPopulationEnergySeam and
                                      block not in borderBlocksToAvoid]

            # If we don't have any neighbors in the direction we're headed,
            # we need to find the next best block candidate
            if len(neighborCandidates) is 0:
                failedStartingBlocks.append(startingBlock)

                remainingStartingCandidates = [startingCandidate for startingCandidate in startingCandidates if
                                               startingCandidate not in failedStartingBlocks]

                # If there are no more starting candidates, remove the adjacent border blocks to avoid rule
                if len(remainingStartingCandidates) is 0:
                    if avoidingAdjacentBorderBlocks:
                        avoidingAdjacentBorderBlocks = False
                        borderBlocksToAvoid = startingCandidates
                        failedStartingBlocks = []
                        startingBlock = min(startingCandidates, key=lambda block: block.populationEnergy)
                        blockToActOn = startingBlock
                        lowestPopulationEnergySeam = [blockToActOn]
                        continue
                    else:  # not sure there is anything more we can do but split everything up
                        # plotGraphObjectGroups([self.children, failedStartingBlocks, finishingBlocksToAvoid])
                        tqdm.write("Can't find a {0} path through {1}. Tried and failed on {2} starting blocks"
                                   .format(alignment, self.graphId, len(failedStartingBlocks)))
                        return None

                startingBlock = min(remainingStartingCandidates, key=lambda block: block.populationEnergy)
                blockToActOn = startingBlock
                lowestPopulationEnergySeam = [blockToActOn]
                continue

            lowestPopulationEnergyNeighbor = min(neighborCandidates, key=lambda block: block.populationEnergy)
            if shouldDrawGraph:
                plotGraphObjectGroups(graphObjectGroups=[self.children,
                                                         lowestPopulationEnergySeam,
                                                         neighborCandidates,
                                                         [lowestPopulationEnergyNeighbor],
                                                         failedStartingBlocks],
                                      showGraphHeatmapForFirstGroup=True,
                                      saveImages=True,
                                      saveDescription='SeamFinding{0}-{1}-{2}'.format(count, alignment, id(self)))

            blockToActOn = lowestPopulationEnergyNeighbor

            lowestPopulationEnergySeam.append(blockToActOn)

            if blockToActOn in finishCandidates:
                finishedSeam = True
                finishingBlock = blockToActOn

            count += 1
        return lowestPopulationEnergySeam, finishingBlock, startingBlockEnergy
Пример #7
0
def weightedForestFireFillGraphObject(candidateObjects,
                                      startingObjects=None,
                                      condition=lambda x, y: (True, 0),
                                      weightingScore=lambda w, x, y, z: 1,
                                      shouldDrawEachStep=False,
                                      returnBestCandidateGroup=True,
                                      fastCalculations=True):
    bestGraphObjectCandidateGroupThisPass = None
    offCount = 0
    candidateGroupsThatDidNotMeetConditionThisPass = []
    fireFilledObjects = []
    fireQueue = []
    remainingObjects = candidateObjects.copy()
    if not startingObjects:
        # this doesn't occur during the forest fire fill when creating districts
        startingObjects = [remainingObjects[0]]
    fireQueue.append(startingObjects)

    count = 1
    with tqdm() as pbar:
        while len(fireQueue) > 0:
            pbar.update(1)
            pbar.set_description(
                'FireFilled: {0} - FireQueue: {1} - Remaining: {2} - Off count: {3}'.format(
                    len(fireFilledObjects), len(fireQueue), len(remainingObjects), offCount))

            # pull from the top of the queue
            graphObjectCandidateGroup = fireQueue.pop(0)

            # remove objects that we pulled from the queue from the remaining list
            remainingObjects = [object for object in remainingObjects if object not in graphObjectCandidateGroup]

            if shouldDrawEachStep:
                plotGraphObjectGroups([fireFilledObjects, graphObjectCandidateGroup, remainingObjects],
                                      showDistrictNeighborConnections=True,
                                      saveImages=True,
                                      saveDescription='WeightedForestFireFillGraphObject-{0}-{1}'.format(
                                          id(candidateObjects), count))
                count += 1

            potentiallyIsolatedGroups = findContiguousGroupsOfGraphObjects(remainingObjects)
            if len(potentiallyIsolatedGroups) <= 1:  # candidate won't block any other groups
                conditionResult = condition(fireFilledObjects, graphObjectCandidateGroup)
                if conditionResult[0]:
                    offCount = conditionResult[1]
                    fireFilledObjects.extend(graphObjectCandidateGroup)
                    bestGraphObjectCandidateGroupThisPass = None  # set this back to none when we add something
                    candidateGroupsThatDidNotMeetConditionThisPass = []  # clear this when we add something

                    # find any of objects just added and remove them from the queue
                    remainingItemsFromGroups = []
                    groupsToRemove = []
                    for queueItemGroup in fireQueue:
                        if any([queueItem for queueItem in queueItemGroup if queueItem in graphObjectCandidateGroup]):
                            remainingItems = [queueItem for queueItem in queueItemGroup if
                                              queueItem not in graphObjectCandidateGroup]
                            remainingItemsFromGroups.extend(remainingItems)
                            groupsToRemove.append(queueItemGroup)
                    # remove duplicates from the lists
                    remainingItemsFromGroups = set(remainingItemsFromGroups)
                    # crazy way to remove duplicates from a list of lists
                    groupsToRemove = [list(item) for item in set(tuple(row) for row in groupsToRemove)]
                    for groupToRemove in groupsToRemove:
                        fireQueue.remove(groupToRemove)
                    neighborsOfFireFilledObjects = [group.allNeighbors for group in fireFilledObjects]
                    for remainingItemFromGroups in remainingItemsFromGroups:
                        if remainingItemFromGroups in neighborsOfFireFilledObjects:
                            fireQueue.append([remainingItemFromGroups])

                    # add neighbors to the queue
                    for graphObjectCandidate in graphObjectCandidateGroup:
                        for neighborObject in graphObjectCandidate.allNeighbors:
                            flatFireQueue = [graphObject for graphObjectGroup in fireQueue
                                             for graphObject in graphObjectGroup]
                            if neighborObject in remainingObjects and neighborObject not in flatFireQueue:
                                fireQueue.append([neighborObject])

                    # if we don't need to return the next best candidate, we can remove groups from the queue
                    # that don't meet the condition right now to speed up processing
                    if not returnBestCandidateGroup:
                        fireQueue = [fireQueueGroup for fireQueueGroup in fireQueue if
                                     condition(fireFilledObjects, fireQueueGroup)[0]]
                else:
                    if returnBestCandidateGroup and bestGraphObjectCandidateGroupThisPass is None:
                        if all([len(graphObjectCandidate.children) > 1
                                for graphObjectCandidate in graphObjectCandidateGroup]):
                            bestGraphObjectCandidateGroupThisPass = graphObjectCandidateGroup

                    remainingObjects.extend(graphObjectCandidateGroup)  # add candidate back to the queue
                    candidateGroupsThatDidNotMeetConditionThisPass.append(graphObjectCandidateGroup)
            else:
                # find the contiguous group with largest population and remove.
                # This everything else and will be handled by subsequent fire fill passes
                potentiallyIsolatedGroups.sort(key=lambda x: sum(group.population for group in x), reverse=True)
                potentiallyIsolatedGroups.remove(potentiallyIsolatedGroups[0])
                potentiallyIsolatedObjects = [group for groupList in potentiallyIsolatedGroups for group in groupList]

                conditionResult = condition(fireFilledObjects, potentiallyIsolatedObjects + graphObjectCandidateGroup)
                if conditionResult[0]:
                    if shouldDrawEachStep:
                        plotGraphObjectGroups(
                            [fireFilledObjects, graphObjectCandidateGroup, remainingObjects,
                             potentiallyIsolatedObjects],
                            showDistrictNeighborConnections=True,
                            saveImages=True,
                            saveDescription='WeightedForestFireFillGraphObject-{0}-{1}'.format(id(candidateObjects),
                                                                                               count))
                        count += 1

                    groupAndIsolatedObjects = potentiallyIsolatedObjects + graphObjectCandidateGroup

                    if groupAndIsolatedObjects not in candidateGroupsThatDidNotMeetConditionThisPass:
                        fireQueue.append(groupAndIsolatedObjects)
                else:
                    candidateGroupsThatDidNotMeetConditionThisPass.append(graphObjectCandidateGroup)

                remainingObjects.extend(graphObjectCandidateGroup)  # add candidate back to the queue

            # remove duplicates from the list
            fireQueue.sort()
            fireQueue = list(fireQueueItem for fireQueueItem, _ in groupby(fireQueue))

            # apply weights for sorting
            weightedQueue = []
            fireFilledObjectsShape = polygonFromMultipleGeometries(fireFilledObjects)
            for queueObjectGroup in fireQueue:
                weightScore = weightingScore(fireFilledObjectsShape, remainingObjects, queueObjectGroup,
                                             fastCalculations)
                weightedQueue.append((queueObjectGroup, weightScore))

            # sort queue
            weightedQueue.sort(key=lambda x: x[1], reverse=True)
            fireQueue = [x[0] for x in weightedQueue]

    if shouldDrawEachStep:
        plotGraphObjectGroups(
            [fireFilledObjects, [], remainingObjects],
            showDistrictNeighborConnections=True,
            saveImages=True,
            saveDescription='WeightedForestFireFillGraphObject-{0}-{1}'.format(id(candidateObjects), count))

    return fireFilledObjects, bestGraphObjectCandidateGroupThisPass