Source code for gerrychain.constraints.contiguity
from collections import deque
from collections.abc import Hashable, Iterable
from .._deprecated import deprecated_alias
from ..graph import FrozenGraph, Graph
from ..partition import Partition
from .bounds import SelfConfiguringLowerBound
def _are_reachable(
graph: Graph | FrozenGraph,
start_node: Hashable,
mapping: dict[Hashable, Hashable],
part: Hashable,
targets: Iterable[Hashable],
) -> bool:
"""Check if the targets are reachable from the start_node without leaving the given district.
The search starts inside ``part`` and only ever steps to neighbors that are also in ``part``,
which is equivalent to (but much cheaper than) calling an avoid-this-edge predicate on every
edge: one dict lookup per neighbor instead of a Python function call plus two lookups.
Args:
graph (Graph): Graph
start_node (Hashable): The starting node; must be in ``part``
mapping (dict[Hashable, Hashable]): The node_id -> part assignment mapping
part (Hashable): The part (district) the search is confined to
targets (Iterable[Hashable]): The target nodes that we would like to reach
Returns:
bool: True if all of the targets are reachable from the start_node node
without leaving ``part``, False otherwise.
"""
# Track the targets not yet reached in a set so the loop condition is O(1)
# per iteration; the search stops as soon as the last target is reached.
unseen_targets = set(targets)
unseen_targets.discard(start_node)
seen = {start_node}
queue = deque((start_node,))
neighbors = graph.neighbors
while unseen_targets and queue:
node_id = queue.popleft()
for neighbor_node_id in neighbors(node_id):
if neighbor_node_id not in seen and mapping[neighbor_node_id] == part:
seen.add(neighbor_node_id)
unseen_targets.discard(neighbor_node_id)
queue.append(neighbor_node_id)
return not unseen_targets
[docs]
def single_flip_contiguous(partition: Partition) -> bool:
"""Check if swapping the given node from its old assignment disconnects the old assignment.
This optimized check assumes that the parent partition is contiguous and that the proposal
changes one node. Use :func:`contiguous` when either assumption does not hold.
Args:
partition (Partition): The proposed next Partition
Returns:
bool: whether the partition is contiguous
"""
parent = partition.parent
flips = partition.flips
if not flips or not parent:
return contiguous(partition)
graph = partition.graph
mapping = partition.assignment.mapping
parent_mapping = parent.assignment.mapping
for changed_node in flips:
old_assignment = parent_mapping[changed_node]
old_neighbors = [
node for node in graph.neighbors(changed_node) if mapping[node] == old_assignment
]
# Under our assumptions, if there are no old neighbors, then the
# old_assignment district has vanished. It is trivially connected.
# We consider the empty district to be disconnected.
if not old_neighbors:
return False
start_neighbor = old_neighbors[0]
# A single old neighbor is trivially reachable from itself.
if len(old_neighbors) == 1:
continue
# Check if all old neighbors in the same assignment are still reachable
# without the search leaving the old district.
connected = _are_reachable(graph, start_neighbor, mapping, old_assignment, old_neighbors)
if not connected:
return False
# All neighbors of all changed nodes are connected, so the new graph is
# connected.
return True
def _affected_parts(partition: Partition) -> set[Hashable]:
"""Checks which partitions were affected by the change of nodes.
Args:
partition (Partition): The proposed next Partition
Returns:
set[Hashable]: The set of IDs of all parts that gained or lost a node when compared to the
parent partition.
"""
flips = partition.flips
parent = partition.parent
if flips is None:
return set(partition.parts)
if parent is None:
return set(flips.values())
affected = set()
for node, part in flips.items():
affected.add(part)
affected.add(parent.assignment.mapping[node])
return affected
[docs]
def contiguous(partition: Partition) -> bool:
"""Check if the parts of a partition are connected.
This function checks if the parts of a partition are connected. It returns whether the
all components of the partition are contiguous.
Args:
partition (Partition): The proposed next Partition
Returns:
bool: Whether the partition is contiguous
"""
# Check connectivity of each part's node set directly rather than going through
# partition.subgraphs[part]: building a full Graph subgraph wrapper costs several
# Python passes over the part's nodes, none of which a yes/no check needs.
graph = partition.graph
parts = partition.parts
return all(graph.is_node_set_connected(parts[part]) for part in _affected_parts(partition))
def number_of_contiguous_parts(partition: Partition) -> int:
"""Computes number of contiguous parts in the partition.
Args:
partition (Partition): Instance of Partition; contains connected components.
Returns:
int: Number of contiguous parts in the partition.
"""
graph = partition.graph
parts = partition.assignment.parts
return sum(1 for part in parts if graph.is_node_set_connected(parts[part]))
# Create an instance of SelfConfiguringLowerBound using the number_of_contiguous_parts function.
# This instance, no_more_discontiguous, is configured to maintain a lower bound on the number of
# contiguous parts in a partition. This is still callable since the class
# SelfConfiguringLowerBound implements the __call__ magic method.
no_more_discontiguous = SelfConfiguringLowerBound(number_of_contiguous_parts)
def contiguous_components(partition: Partition) -> dict[Hashable, list[Graph]]:
"""Determines the connected components of each of the subgraphs of the parts of the partition.
Args:
partition (Partition): Instance of Partition; contains connected components.
Returns:
dict: dictionary mapping each part ID to a list holding the connected subgraphs of that
part of the partition
"""
connected_components_in_each_partition: dict[Hashable, list[Graph]] = {}
for part, subgraph in partition.subgraphs.items():
# create a subgraph for each set of connected nodes in the part's nodes
connected_components_in_each_partition[part] = subgraph.subgraphs_for_connected_components()
return connected_components_in_each_partition
affected_parts = deprecated_alias(
"gerrychain.constraints.contiguity.affected_parts",
"_affected_parts",
_affected_parts,
)
contiguous_bfs = deprecated_alias(
"gerrychain.constraints.contiguous_bfs",
"contiguous",
contiguous,
)