Quickstart: First Chain on a Synthetic Grid

This tutorial walks through the minimum needed to run a FalCom chain. We use a 10×10 synthetic grid so no external data is required.

By the end you’ll know how to:

  1. Build a graph with the required attributes

  2. Construct an initial partition

  3. Run a chain and inspect samples

1. Build a graph

For a real workflow you’d use Graph.from_geodataframe() with your own shapefile or GeoJSON. Here we use the Grid testing utility to skip data prep.

from falcomchain.graph.grid import Grid
from falcomchain.random import set_seed

set_seed(2025)  # for reproducibility
graph = Grid(dimensions=(10, 10), num_candidates=20, density="uniform").graph
print(f"Graph: {graph.number_of_nodes()} nodes, {graph.number_of_edges()} edges")
Graph: 100 nodes, 180 edges

Each node has the required FalcomChain attributes: demand, candidate, C_X, C_Y, area.

sample_node = list(graph.nodes)[0]
graph.nodes[sample_node]
{'demand': 50,
 'C_X': 0,
 'C_Y': 0,
 'area': 1,
 'candidate': False,
 'super_candidate': 0,
 'boundary_node': True,
 'boundary_perim': 2}

2. Initial partition

FalCom samples partitions where:

  • Each district has roughly demand_target × teams total demand (within ε)

  • Each district has between 1 and capacity_level teams

  • Each district contains at least one facility candidate

from falcomchain.partition import Partition

partition = Partition.from_random_assignment(
    graph=graph,
    epsilon=0.2,
    demand_target=500,
    assignment_class=None,
    capacity_level=3,
)
print(f"{len(partition)} districts, teams per district: {dict(partition.teams)}")
5 districts, teams per district: {1: 2, 2: 1, 3: 3, 4: 3, 5: 1}

3. Travel times

The energy function and facility selection use a travel-time matrix. For this synthetic grid we use Manhattan distance — for real use cases, compute travel times once with your preferred method (network distance, GTFS-based transit time, etc.).

from falcomchain.partition.assignment import Assignment

nodes = list(graph.nodes)
Assignment.travel_times = {
    (a, b): float(abs(a[0] - b[0]) + abs(a[1] - b[1]))
    for a in nodes for b in nodes
}

4. Build the initial state and run the chain

from functools import partial
from falcomchain.markovchain.state import ChainState
from falcomchain.markovchain.energy import compute_energy
from falcomchain.markovchain.proposals import hierarchical_recom
from falcomchain.markovchain.accept import always_accept
from falcomchain.markovchain.chain import MarkovChain

state = ChainState.initial(partition=partition, energy=0.0, beta=1.0)
state.energy = compute_energy(state)
print(f"Initial energy: {state.energy:.0f}")

proposal = partial(
    hierarchical_recom,
    epsilon_base=0.2,
    epsilon_super=0.2,
    demand_target=500,
)
chain = MarkovChain(
    proposal=proposal,
    constraints=lambda p: True,
    accept=always_accept,
    initial_state=state,
    total_steps=51,  # 50 steps + initial
)

energies = []
for s in chain:
    energies.append(s.energy)

print(f"Final energy: {energies[-1]:.0f}")
print(f"Min energy:   {min(energies):.0f}")
Initial energy: 13750
Final energy: 0
Min energy:   0

What’s next