Source code for gerrychain.updaters.compactness
from __future__ import annotations
import collections
from collections.abc import Hashable
from typing import TYPE_CHECKING
from .._deprecated import legacy_flips
from .cut_edges import on_edge_flow
from .flows import on_flow
if TYPE_CHECKING:
from ..partition.partition import Partition
flips = legacy_flips
[docs]
def boundary_nodes(partition: Partition, alias: str = "boundary_nodes") -> set[Hashable]:
"""Return set of nodes in the partition that are on the boundary.
Args:
partition (Partition): A partition of a Graph
alias (str, optional): The name of the attribute that the boundary nodes are stored under.
Default is 'boundary_nodes'.
Returns:
set: The set of nodes in the partition that are on the boundary.
"""
# Note that the "alias" parameter is used as the attribute name
# on the partition - using this "alias" you can retrieve the
# the data stored by an updater that uses this routine...
if partition.parent:
return partition.parent[alias]
else:
result = {
node_id
for node_id in partition.graph.node_indices
if partition.graph.node_data(node_id)["boundary_node"]
}
return result
def initialize_exterior_boundaries_as_a_set(partition: Partition) -> dict[Hashable, set[Hashable]]:
"""Return exterior boundary nodes for each part in the partition.
Args:
partition (Partition): A partition of a Graph
Returns:
dict[Hashable, set[Hashable]]: Parts mapped to their exterior boundary nodes.
"""
part_boundaries: collections.defaultdict[Hashable, set[Hashable]] = collections.defaultdict(set)
for node in partition["boundary_nodes"]:
part_boundaries[partition.assignment.mapping[node]].add(node)
return part_boundaries
[docs]
@on_flow(initialize_exterior_boundaries_as_a_set, alias="exterior_boundaries_as_a_set")
def exterior_boundaries_as_a_set(
partition: Partition,
previous: set[Hashable],
inflow: set[Hashable],
outflow: set[Hashable],
) -> set[Hashable]:
"""Updater function that responds to the flow of nodes between different partitions.
Args:
partition (Partition): A partition of a Graph
previous (set[Hashable]): Previous exterior boundary nodes for a fixed part.
inflow (set[Hashable]): Nodes that flowed into the part.
outflow (set[Hashable]): Nodes that flowed out of the part.
Returns:
set[Hashable]: The updated exterior boundary nodes for the part.
"""
# Compute the new set of boundary nodes for the partition.
#
# The term, (inflow & graph_boundary), computes new nodes that are boundary nodes.
#
# the term, (previous | (inflow & graph_boundary)), adds those new boundary nodes to the
# set of previous boundary nodes.
#
# Then all you need to do is subtract all of the nodes in the outflow to remove any of those
# that happen to be boundary nodes...
graph_boundary = partition["boundary_nodes"]
return (previous | (inflow & graph_boundary)) - outflow
def initialize_exterior_boundaries(partition: Partition) -> dict[Hashable, float]:
"""Return A dictionary mapping each part of a partition to the total perimeter of the boundary.
Args:
partition (Partition): A partition of a Graph
Returns:
dict[Hashable, float]: Parts mapped to their exterior boundary perimeter.
"""
graph_boundary = partition["boundary_nodes"]
boundaries: collections.defaultdict[Hashable, float] = collections.defaultdict(float)
for node in graph_boundary:
part = partition.assignment.mapping[node]
boundaries[part] += partition.graph.node_data(node)["boundary_perim"]
return boundaries
[docs]
@on_flow(initialize_exterior_boundaries, alias="exterior_boundaries")
def exterior_boundaries(
partition: Partition,
previous: float,
inflow: set[Hashable],
outflow: set[Hashable],
) -> float:
"""Computes the total perimeter of the boundary nodes in each part of the partition.
Args:
partition (Partition): A partition of a Graph
previous (float): Previous exterior boundary perimeter for the part.
inflow (set[Hashable]): Nodes that flowed into the part.
outflow (set[Hashable]): Nodes that flowed out of the part.
Returns:
float: The updated exterior boundary perimeter for the part.
"""
graph_boundary = partition["boundary_nodes"]
added_perimeter = sum(
partition.graph.node_data(node)["boundary_perim"] for node in inflow & graph_boundary
)
removed_perimeter = sum(
partition.graph.node_data(node)["boundary_perim"] for node in outflow & graph_boundary
)
return previous + added_perimeter - removed_perimeter
def initialize_interior_boundaries(partition: Partition) -> dict[Hashable, float]:
"""Return A dictionary mapping each part of a partition to the total perimeter the given part.
Args:
partition (Partition): A partition of a Graph
Returns:
dict[Hashable, float]: Parts mapped to the perimeter shared with other parts.
"""
# RustworkX Note:
#
# The old NX code did not distinguish between edges and edge_ids - they were one
# and the same. However, in RX an edge is a tuple and an edge_id is an integer.
# The edges stored in partition["cut_edges_by_part"] are edges (tuples), so
# we need to get the edge_id for each edge in order to access the data for the edge.
# Get edge_ids for each edge (tuple)
edge_ids_for_part = {
part: [
partition.graph.get_edge_id_from_edge(edge)
for edge in partition["cut_edges_by_part"][part]
]
for part in partition.parts
}
# Compute length of the shared perimeter of each part
shared_perimeters_for_part = {
part: float(
sum(
partition.graph.edge_data(edge_id)["shared_perim"]
for edge_id in edge_ids_for_part[part]
)
)
for part in partition.parts
}
return shared_perimeters_for_part
[docs]
@on_edge_flow(initialize_interior_boundaries, alias="interior_boundaries")
def interior_boundaries(
partition: Partition,
previous: float,
new_edges: set[tuple[int, int]],
old_edges: set[tuple[int, int]],
) -> float:
"""Computes the total perimeter of the shared boundary between different parts of the partition.
Args:
partition (Partition): A partition of a Graph
previous (float): Previous interior boundary perimeter for the part.
new_edges (set[tuple[int, int]]): Edges that flowed into the part.
old_edges (set[tuple[int, int]]): Edges that flowed out of the part.
Returns:
float: The updated interior boundary perimeter for the part.
"""
added_perimeter = sum(
partition.graph.edge_data(partition.graph.get_edge_id_from_edge(edge))["shared_perim"]
for edge in new_edges
)
removed_perimeter = sum(
partition.graph.edge_data(partition.graph.get_edge_id_from_edge(edge))["shared_perim"]
for edge in old_edges
)
return previous + added_perimeter - removed_perimeter
def perimeter_of_part(partition: Partition, part: Hashable) -> float:
"""Totals up the perimeter of the part in the partition.
.. Warning::
Requires that 'boundary_perim' be a node attribute, 'shared_perim' be an edge attribute,
'cut_edges' be an updater, and 'exterior_boundaries' be an updater.
Args:
partition (Partition): A partition of a Graph
part (Hashable): The part whose perimeter to compute.
Returns:
float: The perimeter of the desired part.
"""
# frm: TODO: Refactoring: Add code to enforce the warning in the docstring above
exterior_perimeter = partition["exterior_boundaries"][part]
interior_perimeter = partition["interior_boundaries"][part]
return exterior_perimeter + interior_perimeter
[docs]
def perimeter(partition: Partition) -> dict[Hashable, float]:
"""Computes the perimeter of each part in the partition.
Args:
partition (Partition): A partition of a Graph
Returns:
dict[Hashable, float]: Parts mapped to their perimeter.
"""
return {part: perimeter_of_part(partition, part) for part in partition.parts}