Skip to content

Commit dff63b5

Browse files
committed
Update docs for main module and rm random.py
Regarding the deletion of the random.py file: this is a fix for issue #364. The problem that the inclusion of the random.py file was supposed to address was the issue of reproducibility of Markov chain, and the idea was to set the global random seed to 2018 at any point where we would like to import the random module internally within the gerrychain package. However, this approach also causes the trees that are generated by the tree.py file to be fixed if the user does not set the random seed after the import of the gerrychain package. So an import pattern of import random random.seed(0) import gerrychain print(random.random()) print(random.random()) will output 0.5331579307274593 0.02768951210200299 as opposed to the expected 0.8444218515250481 0.7579544029403025 will actually force the random seed to be 2018 rather than the expected 0. This can often cause issues in jupyter notebooks where the user is not aware that the random seed has been forcibly set to 2018 after the import of gerrychain. Instead, it is best to allow to user to set the random seed themselves, and to not forcibly set the random seed within the gerrychain package since that can affect the execution of other packages and can cause the chain to hang when the 2018 seed does not produce a valid tree. This issue does not appear if we remove the random.py file and instead use the random module from the standard library within the tree.py and accept.py files. This is because of how python handles successive imports of the same module. Consider the following snipit: import random random.seed(0) import random print(random.random()) print(random.random()) This will output 0.8444218515250481 0.7579544029403025 as expected. This is because the random module is only imported once and then places its name in the internal list of imported modules. Subsequent imports of the random module within the same python session will not will simply retrieve the module from the list and will not re-execute the code contained within the module. Thus, the random seed is only set once and not reset when the random module is imported again. In terms of reproducibility, this means that the user will be required to set the random seed themselves if they want to reproduce the same chain, but this is a relatively standard expectation, and will be required when we move the package over to a rust backend in the future.
1 parent aa02ee4 commit dff63b5

16 files changed

+436
-145
lines changed

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ target/
6363

6464
# pyenv python configuration file
6565
.python-version
66-
66+
.venv
6767
junit.xml
6868

6969
# crapple

docs/conf.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080
#
8181
# This is also used if you do content translation via gettext catalogs.
8282
# Usually you set "language" from the command line for these cases.
83-
language = None
83+
language = 'en'
8484

8585
# List of patterns, relative to source directory, that match files and
8686
# directories to ignore when looking for source files.

gerrychain/accept.py

+15-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
from .random import random
1+
"""
2+
This module provides the main acceptance function used in ReCom Markov chains.
3+
4+
Dependencies:
5+
- random: For random number generation for probabilistic acceptance.
6+
7+
Last Updated: 11 Jan 2024
8+
"""
9+
10+
import random
211
from gerrychain.partition import Partition
312

413

@@ -7,12 +16,15 @@ def always_accept(partition: Partition) -> bool:
716

817

918
def cut_edge_accept(partition: Partition) -> bool:
10-
"""Always accepts the flip if the number of cut_edges increases.
19+
"""
20+
Always accepts the flip if the number of cut_edges increases.
1121
Otherwise, uses the Metropolis criterion to decide.
1222
1323
:param partition: The current partition to accept a flip from.
14-
:return: True if accepted, False to remain in place
24+
:type partition: Partition
1525
26+
:return: True if accepted, False to remain in place
27+
:rtype: bool
1628
"""
1729
bound = 1.0
1830

gerrychain/chain.py

+73-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,26 @@
1+
"""
2+
This module provides the MarkovChain class, which is designed to facilitate the creation
3+
and iteration of Markov chains in the context of political redistricting and gerrymandering
4+
analysis. It allows for the exploration of different districting plans based on specified
5+
constraints and acceptance criteria.
6+
7+
Key Components:
8+
- MarkovChain: The main class used for creating and iterating over Markov chain states.
9+
- Validator: A helper class for validating proposed states in the Markov chain.
10+
see :class:`~gerrychain.constraints.Validator` for more details.
11+
12+
Usage:
13+
The primary use of this module is to create an instance of MarkovChain with appropriate
14+
parameters like proposal function, constraints, acceptance function, and initial state,
15+
and then to iterate through the states of the Markov chain, yielding a new proposal
16+
at each step.
17+
18+
Dependencies:
19+
- typing: Used for type hints.
20+
21+
Last Updated: 11 Jan 2024
22+
"""
23+
124
from .constraints import Validator
225
from typing import Union, Iterable, Callable, Optional
326

@@ -7,8 +30,11 @@
730

831
class MarkovChain:
932
"""
10-
MarkovChain is an iterator that allows the user to iterate over the states
11-
of a Markov chain run.
33+
MarkovChain is a class that creates an iterator for iterating over the states
34+
of a Markov chain run in a gerrymandering analysis context.
35+
36+
It allows for the generation of a sequence of partitions (states) of a political
37+
districting plans, where each partition represents a possible state in the Markov chain.
1238
1339
Example usage:
1440
@@ -30,15 +56,23 @@ def __init__(
3056
) -> None:
3157
"""
3258
:param proposal: Function proposing the next state from the current state.
59+
:type proposal: Callable
3360
:param constraints: A function with signature ``Partition -> bool`` determining whether
3461
the proposed next state is valid (passes all binary constraints). Usually
3562
this is a :class:`~gerrychain.constraints.Validator` class instance.
63+
:type constraints: Union[Iterable[Callable], Validator, Iterable[Bounds], Callable]
3664
:param accept: Function accepting or rejecting the proposed state. In the most basic
3765
use case, this always returns ``True``. But if the user wanted to use a
3866
Metropolis-Hastings acceptance rule, this is where you would implement it.
67+
:type accept: Callable
3968
:param initial_state: Initial :class:`gerrychain.partition.Partition` class.
69+
:type initial_state: Optional[Partition]
4070
:param total_steps: Number of steps to run.
71+
:type total_steps: int
4172
73+
:return: None
74+
75+
:raises ValueError: If the initial_state is not valid according to the constraints.
4276
"""
4377
if callable(constraints):
4478
is_valid = Validator([constraints])
@@ -65,11 +99,34 @@ def __init__(
6599
self.state = initial_state
66100

67101
def __iter__(self) -> 'MarkovChain':
102+
"""
103+
Resets the Markov chain iterator.
104+
105+
This method is called when an iterator is required for a container. It sets the
106+
counter to 0 and resets the state to the initial state.
107+
108+
:return: Returns itself as an iterator object.
109+
:rtype: MarkovChain
110+
"""
68111
self.counter = 0
69112
self.state = self.initial_state
70113
return self
71114

72115
def __next__(self) -> Optional[Partition]:
116+
"""
117+
Advances the Markov chain to the next state.
118+
119+
This method is called to get the next item in the iteration.
120+
It proposes the next state and moves to t it if that state is
121+
valid according to the constraints and if accepted by the
122+
acceptance function. If the total number of steps has been
123+
reached, it raises a StopIteration exception.
124+
125+
:return: The next state of the Markov chain.
126+
:rtype: Optional[Partition]
127+
128+
:raises StopIteration: If the total number of steps has been reached.
129+
"""
73130
if self.counter == 0:
74131
self.counter += 1
75132
return self.state
@@ -88,12 +145,26 @@ def __next__(self) -> Optional[Partition]:
88145
raise StopIteration
89146

90147
def __len__(self) -> int:
148+
"""
149+
Returns the total number of steps in the Markov chain.
150+
151+
:return: The total number of steps in the Markov chain.
152+
:rtype: int
153+
"""
91154
return self.total_steps
92155

93156
def __repr__(self) -> str:
94157
return "<MarkovChain [{} steps]>".format(len(self))
95158

96159
def with_progress_bar(self):
160+
"""
161+
Wraps the Markov chain in a tqdm progress bar.
162+
163+
Useful for long-running Markov chains where you want to keep track
164+
of the progress. Requires the `tqdm` package to be installed.
165+
166+
:return: A tqdm-wrapped Markov chain.
167+
"""
97168
from tqdm.auto import tqdm
98169

99170
return tqdm(self)

gerrychain/constraints/contiguity.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import networkx as nx
55

6-
from ..random import random
6+
import random
77
from .bounds import SelfConfiguringLowerBound
88

99

gerrychain/grid.py

+115-34
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
1-
import math
1+
"""
2+
This module provides a Grid class used for creating and manipulating grid partitions.
3+
It's part of the GerryChain suite, designed to facilitate experiments with redistricting
4+
plans without the need for extensive data processing. This module relies on NetworkX for
5+
graph operations and integrates with GerryChain's Partition class.
6+
7+
Dependencies:
8+
- math: For math.floor() function.
9+
- networkx: For graph operations with using the graph structure in
10+
:class:`~gerrychain.graph.Graph`.
11+
- typing: Used for type hints.
12+
"""
213

14+
import math
315
import networkx
4-
516
from gerrychain.partition import Partition
617
from gerrychain.graph import Graph
718
from gerrychain.updaters import (
@@ -14,8 +25,7 @@
1425
perimeter,
1526
)
1627
from gerrychain.metrics import polsby_popper
17-
18-
from typing import Callable, Dict, Optional, Tuple
28+
from typing import Callable, Dict, Optional, Tuple, Any
1929

2030

2131
class Grid(Partition):
@@ -54,15 +64,35 @@ def __init__(
5464
flips: Optional[Dict[Tuple[int, int], int]] = None,
5565
) -> None:
5666
"""
57-
:param dimensions: tuple (m,n) of the desired dimensions of the grid.
58-
:param with_diagonals: (optional, defaults to False) whether to include diagonals
59-
as edges of the graph (i.e., whether to use 'queen' adjacency rather than
60-
'rook' adjacency).
61-
:param assignment: (optional) dict matching nodes to their districts. If not
62-
provided, partitions the grid into 4 quarters of roughly equal size.
63-
:param updaters: (optional) dict matching names of attributes of the Partition
64-
to functions that compute their values. If not provided, the Grid
65-
configures the cut_edges updater for convenience.
67+
If the updaters are not specified, the default updaters are used, which are as follows::
68+
69+
default_updaters = {
70+
"cut_edges": cut_edges,
71+
"population": Tally("population"),
72+
"perimeter": perimeter,
73+
"exterior_boundaries": exterior_boundaries,
74+
"interior_boundaries": interior_boundaries,
75+
"boundary_nodes": boundary_nodes,
76+
"area": Tally("area", alias="area"),
77+
"polsby_popper": polsby_popper,
78+
"cut_edges_by_part": cut_edges_by_part,
79+
}
80+
81+
82+
:param dimensions: The grid dimensions (rows, columns), defaults to None.
83+
:type dimensions: Tuple[int, int], optional
84+
:param with_diagonals: If True, includes diagonal connections, defaults to False.
85+
:type with_diagonals: bool, optional
86+
:param assignment: Node-to-district assignments, defaults to None.
87+
:type assignment: Dict, optional
88+
:param updaters: Custom updater functions, defaults to None.
89+
:type updaters: Dict[str, Callable], optional
90+
:param parent: Parent Grid object for inheritance, defaults to None.
91+
:type parent: Grid, optional
92+
:param flips: Node flips for partition changes, defaults to None.
93+
:type flips: Dict[Tuple[int, int], int], optional
94+
95+
:raises Exception: If neither dimensions nor parent is provided.
6696
"""
6797
if dimensions:
6898
self.dimensions = dimensions
@@ -100,12 +130,32 @@ def as_list_of_lists(self):
100130
Returns the grid as a list of lists (like a matrix), where the (i,j)th
101131
entry is the assigned district of the node in position (i,j) on the
102132
grid.
133+
134+
:return: List of lists representing the grid.
135+
:rtype: List[List[int]]
103136
"""
104137
m, n = self.dimensions
105138
return [[self.assignment.mapping[(i, j)] for i in range(m)] for j in range(n)]
106139

107140

108-
def create_grid_graph(dimensions: Tuple[int, int], with_diagonals: bool) -> Graph:
141+
def create_grid_graph(
142+
dimensions: Tuple[int, int],
143+
with_diagonals: bool
144+
) -> Graph:
145+
"""
146+
Creates a grid graph with the specified dimensions.
147+
Optionally includes diagonal connections between nodes.
148+
149+
:param dimensions: The grid dimensions (rows, columns).
150+
:type dimensions: Tuple[int, int]
151+
:param with_diagonals: If True, includes diagonal connections.
152+
:type with_diagonals: bool
153+
154+
:return: A grid graph.
155+
:rtype: Graph
156+
157+
:raises ValueError: If the dimensions are not a tuple of length 2.
158+
"""
109159
if len(dimensions) != 2:
110160
raise ValueError("Expected two dimensions.")
111161
m, n = dimensions
@@ -133,12 +183,43 @@ def create_grid_graph(dimensions: Tuple[int, int], with_diagonals: bool) -> Grap
133183
return graph
134184

135185

136-
def give_constant_attribute(graph, attribute, value):
186+
def give_constant_attribute(
187+
graph: Graph,
188+
attribute: Any,
189+
value: Any
190+
) -> None:
191+
"""
192+
Sets the specified attribute to the specified value for all nodes in the graph.
193+
194+
:param graph: The graph to modify.
195+
:type graph: Graph
196+
:param attribute: The attribute to set.
197+
:type attribute: Any
198+
:param value: The value to set the attribute to.
199+
:type value: Any
200+
201+
:return: None
202+
"""
137203
for node in graph.nodes:
138204
graph.nodes[node][attribute] = value
139205

140206

141-
def tag_boundary_nodes(graph: Graph, dimensions: Tuple[int, int]) -> None:
207+
def tag_boundary_nodes(
208+
graph: Graph,
209+
dimensions: Tuple[int, int]
210+
) -> None:
211+
"""
212+
Adds the boolean attribute ``boundary_node`` to each node in the graph.
213+
If the node is on the boundary of the grid, that node also gets the attribute
214+
``boundary_perim`` which is determined by the function :func:`get_boundary_perim`.
215+
216+
:param graph: The graph to modify.
217+
:type graph: Graph
218+
:param dimensions: The dimensions of the grid.
219+
:type dimensions: Tuple[int, int]
220+
221+
:return: None
222+
"""
142223
m, n = dimensions
143224
for node in graph.nodes:
144225
if node[0] in [0, m - 1] or node[1] in [0, n - 1]:
@@ -148,7 +229,23 @@ def tag_boundary_nodes(graph: Graph, dimensions: Tuple[int, int]) -> None:
148229
graph.nodes[node]["boundary_node"] = False
149230

150231

151-
def get_boundary_perim(node: Tuple[int, int], dimensions: Tuple[int, int]) -> int:
232+
def get_boundary_perim(
233+
node: Tuple[int, int],
234+
dimensions: Tuple[int, int]
235+
) -> int:
236+
"""
237+
Determines the boundary perimeter of a node on the grid.
238+
The boundary perimeter is the number of sides of the node that
239+
are on the boundary of the grid.
240+
241+
:param node: The node to check.
242+
:type node: Tuple[int, int]
243+
:param dimensions: The dimensions of the grid.
244+
:type dimensions: Tuple[int, int]
245+
246+
:return: The boundary perimeter of the node.
247+
:rtype: int
248+
"""
152249
m, n = dimensions
153250
if node in [(0, 0), (m - 1, 0), (0, n - 1), (m - 1, n - 1)]:
154251
return 2
@@ -158,7 +255,7 @@ def get_boundary_perim(node: Tuple[int, int], dimensions: Tuple[int, int]) -> in
158255
return 0
159256

160257

161-
def color_half(node, threshold=5):
258+
def color_half(node: Tuple[int, int], threshold: int = 5) -> int:
162259
x = node[0]
163260
return 0 if x <= threshold else 1
164261

@@ -168,19 +265,3 @@ def color_quadrants(node: Tuple[int, int], thresholds: Tuple[int, int]) -> int:
168265
x_color = 0 if x < thresholds[0] else 1
169266
y_color = 0 if y < thresholds[1] else 2
170267
return x_color + y_color
171-
172-
173-
def grid_size(parition):
174-
""" This is a hardcoded population function
175-
for the grid class"""
176-
177-
L = parition.as_list_of_lists()
178-
permit = [3, 4, 5]
179-
180-
sizes = [0, 0, 0, 0]
181-
182-
for i in range(len(L)):
183-
for j in range(len(L[0])):
184-
sizes[L[i][j]] += 1
185-
186-
return all(x in permit for x in sizes)

0 commit comments

Comments
 (0)