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:
Build a graph with the required attributes
Construct an initial partition
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 × teamstotal demand (within ε)Each district has between 1 and
capacity_levelteamsEach 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
Algorithm overview — what FalCom does conceptually
Schema reference — graph attribute requirements
GeoDataFrame guide — using your own geographic data
For animation, save with
Recorderand visualize in FalcomPlot