Source code for gerrychain.constraints.validity

from ..updaters import CountySplit
from .bounds import Bounds
import numpy
from typing import Callable, List, Dict
from ..partition import Partition


[docs]class Validator: """A single callable for checking that a partition passes a collection of constraints. Intended to be passed as the ``is_valid`` parameter when instantiating :class:`~gerrychain.MarkovChain`. This class is meant to be called as a function after instantiation; its return is ``True`` if all validators pass, and ``False`` if any one fails. Example usage:: is_valid = Validator([constraint1, constraint2, constraint3]) chain = MarkovChain(proposal, is_valid, accept, initial_state, total_steps) :ivar constraints: List of validator functions that will check partitions. :type constraints: List[Callable] """ def __init__(self, constraints: List[Callable]) -> None: """ :param constraints: List of validator functions that will check partitions. :type constraints: List[Callable] """ self.constraints = constraints def __call__(self, partition: Partition) -> bool: """ Determine if the given partition is valid. :param partition: The partition to check. :type partition: Partition """ # check each constraint function and fail when a constraint test fails for constraint in self.constraints: is_valid = constraint(partition) # Coerce NumPy booleans if isinstance(is_valid, numpy.bool_): is_valid = bool(is_valid) if is_valid is False: return False elif is_valid is True: pass else: raise TypeError( "Constraint {} returned a non-boolean.".format(repr(constraint)) ) # all constraints are satisfied return True def __repr__(self) -> str: constraint_names = [constraint.__name__ for constraint in self.constraints] return f"Validator(constraints={constraint_names})"
[docs]def within_percent_of_ideal_population( initial_partition: Partition, percent: float = 0.01, pop_key: str = "population" ) -> Bounds: """ Require that all districts are within a certain percent of "ideal" (i.e., uniform) population. Ideal population is defined as "total population / number of districts." :param initial_partition: Starting partition from which to compute district information. :type initial_partition: Partition :param percent: Allowed percentage deviation. Default is 1%. :type percent: float, optional :param pop_key: The name of the population :class:`Tally <gerrychain.updaters.Tally>`. Default is ``"population"``. :type pop_key: str, optional :returns: A :class:`.Bounds` constraint on the population attribute identified by ``pop_key``. :rtype: Bounds """ def population(partition): return partition[pop_key].values() number_of_districts = len(initial_partition[pop_key].keys()) total_population = sum(initial_partition[pop_key].values()) ideal_population = total_population / number_of_districts bounds = ((1 - percent) * ideal_population, (1 + percent) * ideal_population) return Bounds(population, bounds=bounds)
[docs]def deviation_from_ideal( partition: Partition, attribute: str = "population" ) -> Dict[int, float]: """ Computes the deviation of the given ``attribute`` from exact equality among parts of the partition. Usually ``attribute`` is the population, and this function is used to compute how far a districting plan is from exact population equality. By "deviation" we mean ``(actual_value - ideal)/ideal`` (not the absolute value). :param partition: A partition. :type partition: Partition :param attribute: The :class:`Tally <gerrychain.updaters.Tally>` to compute deviation for. Default is ``"population"``. :type attribute: str, optional :returns: dictionary from parts to their deviation :rtype: Dict[int, float] """ number_of_districts = len(partition[attribute].keys()) total = sum(partition[attribute].values()) ideal = total / number_of_districts return { part: (value - ideal) / ideal for part, value in partition[attribute].items() }
[docs]def districts_within_tolerance( partition: Partition, attribute_name: str = "population", percentage: float = 0.1 ) -> bool: """ Check if all districts are within a certain percentage of the "smallest" district, as defined by the given attribute. :param partition: Partition class instance :type partition: Partition :param attrName: String that is the name of an updater in partition. Default is ``"population"``. :type attrName: str, optional :param percentage: What percent (as a number between 0 and 1) difference is allowed. Default is 0.1. :type percentage: float, optional :returns: Whether the districts are within specified tolerance :rtype: bool """ if percentage >= 1: percentage *= 0.01 values = partition[attribute_name].values() max_difference = max(values) - min(values) within_tolerance = max_difference <= percentage * min(values) return within_tolerance
[docs]def refuse_new_splits(partition_county_field: str) -> Callable[[Partition], bool]: """ Refuse all proposals that split a county that was previous unsplit. :param partition_county_field: Name of field for county information generated by :func:`.county_splits`. :type partition_county_field: str :returns: Function that returns ``True`` if the proposal does not split any new counties. :rtype: Callable[[Partition], bool] """ def _refuse_new_splits(partition: Partition) -> bool: for county_info in partition[partition_county_field].values(): if county_info.split == CountySplit.NEW_SPLIT: return False return True return _refuse_new_splits
[docs]def no_vanishing_districts(partition: Partition) -> bool: """ Require that no districts be completely consumed. :param partition: Partition to check. :type partition: Partition :returns: Whether no districts are completely consumed. :rtype: bool """ if not partition.parent: return True return all(len(part) > 0 for part in partition.assignment.parts.values())