Forest ReCom¶
The Forest runner exposes the Multi-Scale Map Sampler (MSMS), the hierarchical ReCom method described in Multi-Scale Merge-Split Markov Chain Monte Carlo for Redistricting. Instead of treating every unit at one resolution, MSMS can use nested geographic levels when building and cutting spanning forests. Counties, municipalities, precincts, or other nested units can therefore be part of the sampling model rather than a score applied afterward.
Input and hierarchy¶
ForestRunnerConfig takes a NetworkX node-link JSON dual graph. Every node needs the
population column named by pop_col and the hierarchy columns listed in levels. List levels
from coarsest to finest, for example county followed by precinct. A one-element list runs a
flat chain; additional levels allow a deeper hierarchy.
Hierarchy labels describe membership, not numeric measurements. Within each level, the labels
should consistently identify the containing unit for every graph node. The shipped
data/mgrp_7x7.json graph contains the population and hierarchy used below.
from gerrytools.mgrp import ForestRunnerConfig, ForestRunSpec
config = ForestRunnerConfig(
json_file_path="data/mgrp_7x7.json",
output_folder="output",
log_folder="logs",
)
run = ForestRunSpec(
levels=["county", "precinct"],
pop_col="TOTPOP",
num_dists=4,
pop_dev=0.2,
gamma=0.0,
n_steps=100,
rng_seed=2026,
writer="jsonl",
)
The main run settings are:
Setting |
Meaning |
|---|---|
|
Hierarchy columns, ordered from coarsest to finest |
|
Node attribute containing districting population |
|
Number of districts in every sampled plan |
|
Maximum population deviation admitted by the sampler |
|
MSMS parameter interpolating between spanning-forest and partition weighting |
|
Length of the Markov chain |
|
Native random-number-generator seed |
At gamma=0, MSMS samples uniformly over the relevant spanning forests; at gamma=1, the
weighting is uniform over partitions. Intermediate values change that weighting and
should be treated as part of the model specification.
Run the sampler once Docker is available and the configured graph exists:
from gerrytools.mgrp import RunnerSession
with RunnerSession(config) as container:
output_path = container.run(run)
The container runs the Julia engine, captures stderr in the resolved log file, verifies the expected outputs, and returns the primary host output path.
Output formats¶
Forest has three output formats:
Writer |
Output |
|---|---|
|
Standard assignment records, one JSON object per sample |
|
Compact BEN assignment stream |
|
MSMS atlas output without the GerryTools parser stage |
For jsonl and ben, the runner pipes the raw atlas output through a parser and writes a
metadata JSONL sidecar with parser provenance. Raw atlas output can be substantially larger
and retains the engine-specific representation, so it is mainly useful when another MSMS tool
requires that format.
output_file_name overrides the derived primary name. Otherwise the runner includes the seed,
gamma, step count, and a short hash of the full effective configuration. force_print=True is
available for text output, but is incompatible with the binary BEN writer.
Hierarchy constraints¶
Forest constraints act during sampling, and one Constraints builder can combine them. Each
describes a different property of the hierarchy:
Builder |
Contract |
|---|---|
|
Require population-heavy hierarchy nodes to satisfy packed-district feasibility targets |
|
Bound total excess district intersections across top-level nodes |
|
Bound excess intersections separately within each top-level node |
|
Require connected traversal through partially occupied hierarchy nodes |
The two split constraints are not interchangeable. max_coarse_node_splits() sets one global
budget, while allowed_excess_dists_in_coarse_nodes() applies a population-derived allowance
to each coarse node.
from gerrytools.mgrp import Constraints
hierarchy_constraints = (
Constraints()
.pack_nodes(unpack=0)
.max_coarse_node_splits(max_splits=5)
.max_discontinuous_traversal_segments()
)
constrained_run = ForestRunSpec(
levels=["county", "precinct"],
pop_col="TOTPOP",
num_dists=4,
pop_dev=0.2,
gamma=0.0,
constraints=hierarchy_constraints,
n_steps=100,
rng_seed=2026,
)
The constraint builders validate argument types and numeric domains immediately. Feasibility still depends on the graph, population, hierarchy, and district count, so a restrictive combination can leave the sampler with few or no valid moves. The MGRP API documents the exact statistic used by each builder.
Apply GerryChain updaters while streaming¶
Forest is an MCMC runner, so it can stream canonical assignments through GerryChain updaters.
mcmc_run_with_updaters() reconstructs each assignment as a Partition and yields the updater
values. The graph node labels must be exactly the integers 0 through n - 1 in ascending
order because the assignment vectors are positional.
def district_count(partition):
return len(partition.parts)
streaming_run = ForestRunSpec(
levels=["county", "precinct"],
pop_col="TOTPOP",
num_dists=4,
pop_dev=0.2,
gamma=0.0,
n_steps=100,
updaters={"district_count": district_count},
)
from gerrytools.mgrp import RunnerSession
with RunnerSession(config) as container:
for result, error in container.mcmc_run_with_updaters(streaming_run):
if error is not None:
print(error, end="")
elif result is not None:
print(result)
This method is appropriate when a metric already exists as a Python updater and only its value needs to be retained. For large ensembles, writing BEN and scoring afterward avoids rebuilding every plan in Python during the native run.
Inspect the effective run¶
run_config() returns the complete document sent to the Forest CLI. It includes the mounted
container paths, writer, run settings, and normalized constraint list. output_file(),
expected_files(), and log_file() resolve host artifacts before a
container starts.
config.run_config(constrained_run)