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

levels

Hierarchy columns, ordered from coarsest to finest

pop_col

Node attribute containing districting population

num_dists

Number of districts in every sampled plan

pop_dev

Maximum population deviation admitted by the sampler

gamma

MSMS parameter interpolating between spanning-forest and partition weighting

n_steps

Length of the Markov chain

rng_seed

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

jsonl

Standard assignment records, one JSON object per sample

ben

Compact BEN assignment stream

raw

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

pack_nodes()

Require population-heavy hierarchy nodes to satisfy packed-district feasibility targets

max_coarse_node_splits()

Bound total excess district intersections across top-level nodes

allowed_excess_dists_in_coarse_nodes()

Bound excess intersections separately within each top-level node

max_discontinuous_traversal_segments()

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)