FalcomChain API
falcomchain.graph
- class falcomchain.graph.Graph(*args, backend=None, **kwargs)[source]
Represents a graph to be partitioned, extending the
networkx.Graph.This class includes additional class methods for constructing graphs from shapefiles, and for saving and loading graphs in JSON format.
- classmethod from_networkx(graph)[source]
Create a Graph instance from a networkx.Graph object.
- Parameters:
graph (networkx.Graph) – The networkx graph to be converted.
- Returns:
The converted graph as an instance of this class.
- Return type:
- classmethod create(source, **kwargs)[source]
Smart constructor that dispatches based on the type of
source.str(filename ending in .shp, .geojson, .gpkg, etc.) ->from_filegeopandas.GeoDataFrame->from_geodataframenetworkx.Graph->from_networkxdictwith keyedges->from_dataexisting
Graph-> returned as-is
- Parameters:
source – Input data of various types.
kwargs – Forwarded to the underlying constructor.
- Returns:
A Graph object.
- Return type:
Example:
graph = Graph.create("districts.shp", demand_col="POP") graph = Graph.create(my_geodataframe, candidate_col="is_candidate") graph = Graph.create({"edges": [(1,2),(2,3)], "demand": {1:10, 2:20, 3:30}, "candidates": [1, 3]})
- classmethod from_data(edges, demand, candidates=None, super_candidates=None, coordinates=None, area=None, extra_attributes=None, validate=True)[source]
Build a Graph from raw data dictionaries. The recommended entry point for users who don’t have a GeoDataFrame.
- Parameters:
edges – Iterable of (u, v) tuples defining the adjacency.
demand – Dict mapping node -> demand value.
candidates – Iterable of node IDs that are level-1 facility candidates (F^1 ⊂ V^1), or dict node -> bool. Defaults to no candidates.
super_candidates – Iterable of node IDs that are level-2 (super-) facility candidates (F^2 ⊂ V^1), or dict node -> bool. Independent of
candidates— a node can be in either, both, or neither. Defaults to no super-candidates.coordinates – Dict mapping node -> (x, y). Defaults to (0, 0).
area – Dict mapping node -> area. Defaults to 1.0.
extra_attributes – Dict of {attribute_name: {node: value}} for additional node attributes (e.g., custom features).
validate (bool)
- Returns:
A Graph with node attributes
demand,area,candidate,super_candidate,C_X,C_Y, plus any extras.- Return type:
Example:
graph = Graph.from_data( edges=[(1, 2), (2, 3), (3, 1)], demand={1: 100, 2: 150, 3: 80}, candidates=[1, 3], # level-1 candidates super_candidates=[3], # level-2 candidate (must be V^1) coordinates={1: (0, 0), 2: (1, 0), 3: (0.5, 1)}, )
- set_partition(assignment, teams_per_district=None, capacity_level=None)[source]
Write partition state into the graph as node and graph attributes. After calling this, the graph IS the partition — you can serialize it as a single artifact.
- Parameters:
assignment – Dict mapping node -> district ID.
teams_per_district – Optional dict district -> team count.
capacity_level – Optional max teams per district.
- get_partition_data()[source]
Read partition state from graph attributes. Returns None if no
districtattribute is present.- Returns:
Dict with
assignment,teams_per_district,capacity_level, or None if no partition data exists.
- classmethod from_json(json_file)[source]
Load a graph from a JSON file in the NetworkX json_graph format.
- to_json(json_file, *, include_geometries_as_geojson=False)[source]
Save a graph to a JSON file in the NetworkX json_graph format.
- Parameters:
json_file (str) – Path to target JSON file.
include_geometry_as_geojson (bool) – Whether to include any
shapelygeometry objects encountered in the graph’s node attributes as GeoJSON. The default (False) behavior is to remove all geometry objects because they are not serializable. Including the GeoJSON will result in a much larger JSON file.include_geometries_as_geojson (bool, optional)
- Returns:
None
- Return type:
None
- classmethod from_file(filename, adjacency='rook', demand_col='demand', candidate_col='candidate', super_candidate_col='super_candidate', area_col=None, cols_to_add=None, reproject=False, ignore_errors=False, validate=True)[source]
Create a
Graphfrom a shapefile (or GeoPackage, or GeoJSON, or any other library thatgeopandascan read. Seefrom_geodataframe()for more details.- Parameters:
filename (str) – Path to the shapefile / GeoPackage / GeoJSON / etc.
adjacency (str, optional) – The adjacency type to use (“rook” or “queen”). Default is “rook”
cols_to_add (Optional[List[str]], optional) – The names of the columns that you want to add to the graph as node attributes. Default is None.
reproject (bool, optional) – Whether to reproject to a UTM projection before creating the graph. Default is False.
ignore_errors (bool, optional) – Whether to ignore all invalid geometries and try to continue creating the graph. Default is False.
demand_col (str)
candidate_col (str)
super_candidate_col (str | None)
area_col (str | None)
validate (bool)
- Returns:
The Graph object of the geometries from filename.
- Return type:
Warning
This method requires the optional
geopandasdependency. This method requiresgeopandas. Install it with:pip install geopandas
- classmethod from_geodataframe(dataframe, adjacency='rook', demand_col='demand', candidate_col='candidate', super_candidate_col='super_candidate', area_col=None, cols_to_add=None, reproject=False, ignore_errors=False, crs_override=None, validate=True)[source]
Creates the adjacency
Graphof geometries described by dataframe. The areas of the polygons are included as node attributes (with key area). The shared perimeter of neighboring polygons are included as edge attributes (with key shared_perim). Nodes corresponding to polygons on the boundary of the union of all the geometries (e.g., the state, if your dataframe describes VTDs) have a boundary_node attribute (set to True) and a boundary_perim attribute with the length of this “exterior” boundary.By default, areas and lengths are computed in a UTM projection suitable for the geometries. This prevents the bizarro area and perimeter values that show up when you accidentally do computations in Longitude-Latitude coordinates. If the user specifies reproject=False, then the areas and lengths will be computed in the GeoDataFrame’s current coordinate reference system. This option is for users who have a preferred CRS they would like to use.
- Parameters:
dataframe (
geopandas.GeoDataFrame) – The GeoDataFrame to convert.adjacency (str) – The adjacency type to use (“rook” or “queen”). Default “rook”.
demand_col (str) – Name of the column holding demand values. The column is renamed to
demandon the resulting graph. Default “demand”.candidate_col (str) – Name of the column holding level-1 facility candidate flags (0/1 or True/False). Renamed to
candidate. Default “candidate”.super_candidate_col (str | None) – Name of the column holding level-2 (super-) facility candidate flags. Renamed to
super_candidate. Optional — if the column is absent from the dataframe, every node getssuper_candidate=0. Default “super_candidate”.area_col (str | None) – Name of the column holding pre-computed areas. If None, areas are computed from geometry. Default None.
cols_to_add (List[str] | None) – Additional columns to copy as node attributes.
reproject (bool) – Reproject to a UTM projection before creating the graph.
ignore_errors (bool) – Ignore invalid geometries.
crs_override (str | int | None) – Override the CRS of the GeoDataFrame.
validate (bool) – Verify required FalcomChain attributes after construction.
- Returns:
A Graph with required attributes
demandandcandidate, plus computedarea,boundary_node,boundary_perim, and edgeshared_perim.- Return type:
- Raises:
SchemaValidationError – If
validate=Trueand required attributes are missing or invalid.
- lookup(node, field)[source]
Lookup a node/field attribute.
- Parameters:
node (Any) – Node to look up.
field (Any) – Field to look up.
- Returns:
The value of the attribute field at node.
- Return type:
Any
- property node_indices
- property edge_indices
- add_data(df, columns=None)[source]
Add columns of a DataFrame to a graph as node attributes by matching the DataFrame’s index to node ids.
- Parameters:
df (
pandas.DataFrame) – Dataframe containing given columns.columns (Optional[Iterable[str]], optional) – List of dataframe column names to add. Default is None.
- Returns:
None
- Return type:
None
- join(dataframe, columns=None, left_index=None, right_index=None)[source]
Add data from a dataframe to the graph, matching nodes to rows when the node’s left_index attribute equals the row’s right_index value.
- Parameters:
- Columns:
The columns whose data you wish to add to the graph. If not provided, all columns are added. Default is None.
- Left_index:
The node attribute used to match nodes to rows. If not provided, node IDs are used. Default is None.
- Right_index:
The DataFrame column name to use to match rows to nodes. If not provided, the DataFrame’s index is used. Default is None.
- Returns:
None
- Return type:
None
- class falcomchain.graph.FrozenGraph(graph)[source]
Represents an immutable graph to be partitioned. It is based off
Graph.This speeds up chain runs and prevents having to deal with cache invalidation issues. This class behaves slightly differently than
Graphornetworkx.Graph.Not intended to be a part of the public API.
- Variables:
graph – The underlying graph.
size – The number of nodes in the graph.
- Parameters:
graph (Graph)
Note
The class uses __slots__ for improved memory efficiency.
- class falcomchain.graph.Grid(dimensions, num_candidates, density, threshold=None, candidate_ignore=None)[source]
Synthetic grid graph generator for testing and demonstrations.
Note
This is a testing/demo utility, not a primary entry point. For production use cases, build your graph with
Graph.from_geodataframe()(geographic data) orGraph.from_data()(raw data).Creates an m x n grid graph with all required FalcomChain node attributes (demand, area, C_X, C_Y, candidate) plus boundary information.
Example usage:
grid = Grid(dimensions=(10, 10), num_candidates=20, density="uniform") graph = grid.graph # the underlying Graph object
- Parameters:
num_candidates (int) – Number of nodes randomly selected as facility candidates.
density (str) – Demand pattern: ‘uniform’, ‘opposite’, or ‘corners’.
threshold (tuple | None) – For non-uniform density, threshold tuple.
candidate_ignore (int | None) – Optional region to exclude from candidate sampling.
Node attributes set: demand, area, C_X, C_Y, candidate, boundary_node, boundary_perim. Edge attributes set: shared_perim.
- create_grid_graph(dimensions)[source]
Creates a grid graph with the specified dimensions. Optionally includes diagonal connections between nodes.
- Parameters:
- Returns:
A grid graph.
- Return type:
- Raises:
ValueError – If the dimensions are not a tuple of length 2.
- assign_coordinates()[source]
Sets the specified attribute to the specified value for all nodes in the graph.
- Parameters:
graph (Graph) – The graph to modify.
attribute (Any) – The attribute to set.
value (Any) – The value to set the attribute to.
- Returns:
None
- Return type:
None
- assign_candidates()[source]
Sets self.num_candidates many nodes as candidates uniformly random on permitted region
- Return type:
None
- tag_boundary_nodes(dimensions)[source]
Adds the boolean attribute
boundary_nodeto each node in the graph. If the node is on the boundary of the grid, that node also gets the attributeboundary_perimwhich is determined by the functionget_boundary_perim().
- get_boundary_perim(dimensions)[source]
Determines the boundary perimeter of a node on the grid. The boundary perimeter is the number of sides of the node that are on the boundary of the grid.
- assign_population(dimensions, threshold)[source]
Assigns a color (as an integer) to a node based on its x-coordinate.
This function is used to partition the grid into two parts based on a given threshold. Nodes with an x-coordinate less than or equal to the threshold are assigned one color, and nodes with an x-coordinate greater than the threshold are assigned another.
- Parameters:
- Returns:
An integer representing the color of the node. Returns 0 for nodes with x-coordinate less than or equal to the threshold, and 1 otherwise.
- Return type:
Attribute schema for FalcomChain graphs.
Defines exactly what node, edge, and graph-level attributes the hierarchical capacitated facility location algorithms read and write.
This is the single source of truth — every constructor, validator, and documentation page references this schema.
- class falcomchain.graph.schema.AttributeSpec(name, required, type, default, purpose, validator=None)[source]
Specification for a single attribute.
- exception falcomchain.graph.schema.SchemaValidationError[source]
Raised when a graph’s attributes don’t match the FalcomChain schema.
- falcomchain.graph.schema.required_node_attributes()[source]
List of node attribute names that MUST be present.
- Return type:
- falcomchain.graph.schema.validate_graph(graph, strict=True)[source]
Validate that a graph satisfies the FalcomChain attribute schema.
- Parameters:
graph – A networkx-like Graph.
strict (bool) – If True, raise SchemaValidationError on failure. If False, return a list of error strings.
- Returns:
List of error messages (empty if valid).
- Raises:
SchemaValidationError – If strict=True and validation fails.
- Return type:
falcomchain.partition
- class falcomchain.partition.Partition(capacity_level=None, graph=None, flip=None, superflip=None, parent=None, assignment=None)[source]
Partition represents a partition of the nodes of the graph. It will perform the first layer of computations at each step in the Markov chain - basic aggregations and calculations that we want to optimize.
- Variables:
graph – The underlying graph.
assignment – Maps node IDs to district IDs.
parts – Maps district IDs to the set of nodes in that district.
subgraphs – Maps district IDs to the induced subgraph of that district.
- Parameters:
capacity_level (int | None)
- subgraphs
- classmethod from_random_assignment(graph, epsilon, demand_target, assignment_class, capacity_level=1, density=None, super_assignment=None, init_super_partition=False, epsilon_super=None, c_min=1, c_min_super=1, c_max_super=None, min_districts_super=1, rule='per_team', enforce_global_balance=False, psi_fn=None, super_psi_fn=None)[source]
Create a Partition with a random assignment of nodes to districts.
- Parameters:
graph (
Graph) – The graph to create the Partition from.teams (int) – The total of number of doctor-nurse teams to hire at centers
capacity_level (int) – The maximum number of doctor nurse teams at a facility
epsilon (float) – The maximum relative population deviation from the ideal population. Should be in [0,1].
demand_col (str) – The column of the graph’s node data that holds the demand data.
updaters (Optional[Dict[str, Callable]], optional) – Dictionary of updaters
use_default_updaters (bool, optional) – If False, do not include default updaters.
method (Callable, optional) – The function to use to partition the graph into
n_parts. Defaults tocapacitated_recursive_tree().super_assignment (Dict | None) – Optional dict mapping each base node to a superdistrict ID (e.g. health-zone). When provided, level-1 districts are formed by partitioning each superdistrict independently — every level-1 district lives entirely inside one superdistrict, and the resulting
super_assignmenton the Partition reflects this fixed grouping. WhenNone(default), the graph is partitioned globally and each level-1 district becomes its own superdistrict (identity level-2).init_super_partition (bool) – When
True(andsuper_assignmentis not provided), additionally call recursive partitioning on the supergraph to produce a non-trivial initial level-2 partition (paper Section 5.1, “Initialization”). DefaultFalsekeeps the cheap identity grouping. Ignored whensuper_assignmentis provided (the user-supplied grouping is already a real level-2 partition).epsilon_super (float | None) – Level-2 demand-balance tolerance ε² used by
init_super_partition. Defaults toepsilonwhen not set.c_min (int) – Minimum capacity per district (paper’s
c^ℓ_min). Default 1. Withc_min > 1no district has fewer thanc_minteams; the chain explores a smaller state space and Assumption 6.1 relaxes by a factor ofc_min.demand_target (int)
assignment_class (Assignment)
density (float | None)
c_min_super (int)
c_max_super (int | None)
min_districts_super (int)
rule (str)
enforce_global_balance (bool)
psi_fn (Callable | None)
super_psi_fn (Callable | None)
- Returns:
The partition created with a random assignment
- Return type:
- perform_flip(flipp, superflipp)[source]
Returns the new partition obtained by performing the given flips and new_teams. on this partition. :param flip: :param superflip: :returns: the new
Partition
- crosses_parts(edge)[source]
- Parameters:
edge (Tuple) – tuple of node IDs
- Returns:
True if the edge crosses from one part of the partition to another
- Return type:
- property parts
- property teams
- property candidates
- property super_parts
Maps superdistrict ID to the frozenset of level-1 district IDs in it.
- property super_teams
Maps superdistrict ID to the total number of teams in it.
- save(path)[source]
Serializes the partition’s assignment, team allocations, and metadata to a pickle file.
- Parameters:
path (str) – File path to write to.
- save_json(path)[source]
Save the partition as a human-readable JSON file.
The file contains the full assignment (node -> district), team allocations (district -> teams), per-district demand, and metadata. Node and district IDs are converted to strings for JSON compatibility.
- Parameters:
path (str) – File path to write to.
- write_to_graph(graph=None)[source]
Write this partition’s state into the graph as node and graph attributes. After this, the graph alone fully describes the partition.
- Parameters:
graph – Optional Graph (or networkx.Graph) to write to. Defaults to
self.graph.graph(the underlying mutable graph).
- classmethod from_graph(graph)[source]
Construct a Partition from a graph that has
districtnode attributes andteams_per_districtgraph attribute. This is the inverse ofwrite_to_graph().- Parameters:
graph – A Graph (or networkx.Graph) with partition state stored as attributes.
- Returns:
A Partition instance.
- Return type:
- Raises:
ValueError – If the graph lacks the required
districtattribute.
- classmethod load_partition(graph_path, partition_path)[source]
Loads a partition from saved graph and partition pickle files.
- graph
- capacity_level
- supergraph
- assignment
- super_assignment
- parent
- superflip
- flip
- flow
- step
- class falcomchain.partition.assignment.Assignment(parts, candidates, teams, mapping=None, validate=True)[source]
An assignment of nodes into parts.
The goal of
Assignmentis to provide an interface that mirrors a dictionary (what we have been using for assigning nodes to districts) while making it convenient/cheap to access the set of nodes in each part.An
Assignmenthas apartsproperty that is a dictionary of the form{part: <frozenset of nodes in part>}.- copy()[source]
Returns a copy of the assignment. Does not duplicate the frozensets of nodes, just the parts dictionary.
- Return type:
- facility_assignment(part)[source]
Find the best facility center for
partby minimising the maximum travel time from the candidate to any node in the part (minimax radius).- Returns:
(best_candidate, radius)- Return type:
- update_flows(flow, team_flips)[source]
Update the assignment using the given Flow object.
- Parameters:
flow (
Flow) – The Flow computed from the parent partition.team_flips (Dict) – Maps district IDs to updated team counts.
- classmethod from_dict(assignment, graph, teams)[source]
Create an
Assignmentfrom a dictionary. This is probably the method you want to use to create a new assignment.This also works for
pandas.Series.- Parameters:
- Returns:
A new instance of
Assignmentwith the same assignments as the passed-in dictionary.- Return type:
- class falcomchain.partition.flows.Flow(node_flows, part_flows, candidate_flows)[source]
Groups the three incremental flow dictionaries computed between successive Partition steps. Avoids recomputing the full partition from scratch each step.
- Variables:
node_flows – Maps each part to {“in”: set of nodes that joined, “out”: set that left}.
part_flows – {“in”: set of new district IDs, “out”: set of removed district IDs}.
candidate_flows – Maps each part to {“in”: set of candidate nodes that joined, “out”: set that left}.
- Parameters:
falcomchain.tree
- class falcomchain.tree.tree.SpanningTree(graph, params, supergraph=False)[source]
A class representing a spanning tree with population and density information of its subtrees.
- Variables:
graph – The underlying graph structure.
subsets – A dictionary mapping nodes to their subsets.
population – A dictionary mapping nodes to their populations.
total_demand – The total demand of the graph.
ideal_demand – The ideal demand for each district.
epsilon – The tolerance for demand deviation from the ideal demand within each district.
predecessors – A dictionary mapping nodes to their BFS predecessors.
successors – A dictionary mapping nodes to their BFS successors.
- Parameters:
- has_ideal_density(node)[source]
Checks if the subtree beneath a node has an ideal density up to tolerance ‘density’.
- psi(node)[source]
Candidate-awareness score for the subtree rooted at
node:psi(T_u) = phi(u) * exp(-gamma * r(T_u))
where phi(u) is the facility indicator (number of candidates in subtree) and r(T_u) = min_{f in F_H ∩ T_u} e(f, T_u) is the demand radius — the minimum eccentricity over all facility candidates in T_u.
When gamma = 0 this reduces to phi(u) (pure feasibility score). When travel_times is None, uses 1/phi as a proxy for r(T_u).
- Return type:
- class falcomchain.tree.tree.CutParams(ideal_demand, epsilon, capacity_level, n_teams, two_sided=False, gamma=0.0, travel_times=None, psi_fn=None, c_min=1, super_psi_fn=None, recorder=None, rule='per_team', debt=0.0, min_districts_super=1)[source]
Cut parameters for a spanning tree bipartition. Separates algorithmic settings from the tree structure itself.
- Variables:
ideal_demand – Target demand per team.
epsilon – Allowed relative deviation from ideal_demand.
capacity_level – Maximum teams per district.
n_teams – Total teams to allocate across the graph.
two_sided – If True, both sides of the cut must be balanced.
gamma – Tuning parameter for psi score. 0 -> psi = phi (feasibility count only).
travel_times – Dict (facility, node) -> travel time. None falls back to proxy.
psi_fn – Optional custom psi scoring function(phi, gamma, radius) -> float.
- Parameters:
- class falcomchain.tree.tree.Cut(node, subnodes, assigned_teams, demand, psi)
Represents one admissible cut of a spanning tree.
- Fields:
node – root node of the cut subtree subnodes – frozenset of nodes on one side of the cut assigned_teams – number of doctor-nurse teams for this side demand – total demand of subnodes psi – candidate-awareness score ψ(e) = φ(u) · exp(-γ · r(T_u))
- assigned_teams
Alias for field number 2
- demand
Alias for field number 3
- node
Alias for field number 0
- psi
Alias for field number 4
- subnodes
Alias for field number 1
- class falcomchain.tree.tree.Flip(flips: Dict[Any, Any]=<factory>, team_flips: Dict[Any, Any]=<factory>, new_ids: frozenset = <factory>, merged_ids: frozenset = <factory>, log_proposal_ratio: float = 0.0)[source]
- falcomchain.tree.tree.uniform_spanning_tree(graph)[source]
Builds a spanning tree chosen uniformly from the space of all spanning trees of the graph using Wilson’s algorithm (loop-erased random walk).
- Parameters:
graph (nx.Graph) – Networkx Graph
- Returns:
A spanning tree of the graph chosen uniformly at random.
- Return type:
nx.Graph
- falcomchain.tree.tree.random_spanning_tree(graph)[source]
Builds a spanning tree chosen by Kruskal’s method using random weights.
- Parameters:
graph (nx.Graph) – The input graph to build the spanning tree from. Should be a Networkx Graph.
- Returns:
The minimum spanning tree represented as a Networkx Graph.
- Return type:
nx.Graph
- falcomchain.tree.tree.bipartition_tree(graph, demand_target, epsilon, capacity_level, n_teams, two_sided, supergraph, iteration=0, density=None, max_attempts=5000, allow_pair_reselection=False, initial=False, tree_sampler=None, gamma=0.0, travel_times=None, psi_fn=None, super_psi_fn=None, recorder=None, c_min=1, rule='per_team', debt=0.0, enforce_global_balance=False, tau_global=None, d_bar_orig=None, min_districts_super=1)[source]
Finds a balanced 2-partition of a graph by drawing a spanning tree and finding an edge to cut that leaves at most an epsilon imbalance between the demands of the parts. If no valid cut exists, a new tree is drawn.
- Parameters:
graph (Graph) – The graph to partition.
demand_target (int | float) – The target demand for the returned subset of nodes.
epsilon (float) – The allowable deviation from
demand_target.capacity_level (int) – Maximum teams per district.
n_teams (int) – Total teams for the subgraph.
two_sided (bool) – If True, both sides of the cut must be balanced.
supergraph (bool) – If True, use supergraph admissibility conditions.
tree_sampler – Callable(nx.Graph) -> nx.Graph that produces a spanning tree. Defaults to
uniform_spanning_tree(Wilson’s algorithm).gamma (float) – Candidate-awareness tuning parameter (>= 0). Default 0 (uniform).
travel_times – Dict (facility, node) -> travel time for psi computation.
psi_fn – Optional level-1 scoring function
(phi, gamma, radius) -> float.super_psi_fn – Optional level-2 scoring function
(subnodes, teams) -> floatused byfind_superedge_cutswhensupergraph=True. Defaults to ϕ²(T_u) = teams (paper γ=0 case). Seefalcomchain.markovchain.super_scoringfor the paper’s Eq. 27 hub-coherence factory.max_attempts – Maximum spanning tree samples before giving up.
allow_pair_reselection (bool) – If True, raise ReselectException instead of RuntimeError.
iteration (int)
density (float | None)
c_min (int)
rule (str)
debt (float)
enforce_global_balance (bool)
tau_global (float | None)
d_bar_orig (float | None)
min_districts_super (int)
- Returns:
A (Cut, log_cut_ratio) tuple.
- Raises:
RuntimeError – If no valid cut found after
max_attempts.- Return type:
- falcomchain.tree.tree.capacitated_recursive_tree(graph, n_teams, demand_target, epsilon, capacity_level, density=None, supergraph=False, assignments=None, merged_ids=None, max_id=0, iteration=0, recorder=None, super_psi_fn=None, c_min=1, max_attempts=5000, gamma=0.0, travel_times=None, psi_fn=None, rule='per_team', enforce_global_balance=False, min_districts_super=1)[source]
Recursively partitions a graph into balanced districts using bipartition_tree.
- Parameters:
graph (Graph) – The graph to partition into
len(parts)\(epsilon\)-balanced parts.filtered_parts
n_parts
n_teams (int) – Total number of doctor-nurse teams for all facilities.
demand_target (int) – Target demand for each part of the partition.
column_names
epsilon (float) – How far (as a percentage of
demand_target) fromdemand_targetthe parts of the partition can be.capacity_level (int) – The maximum number of doctor-nurse teams in a facility, If it is 1, n_teams many districts are created.
density – Defaluts to None.
ids – set of ids whose districts are merged
assignments – Old assignments for the nodes of
graph.max_id – maximum district id that has been used before
c_min (int)
max_attempts (int)
gamma (float)
rule (str)
enforce_global_balance (bool)
min_districts_super (int)
- Returns:
New assignments for the nodes of
graph.- Return type:
- class falcomchain.tree.snapshot.Recorder(output_dir, record_substeps=False)[source]
Records chain iterations in a compact binary format.
The format uses delta encoding: only changed nodes are stored per step. Substep data (spanning trees, cuts) is stored in a separate file.
- Parameters:
- write_header(graph, initial_partition, params)[source]
Write the file header, node ID table, and initial assignment. Must be called before any record_step calls.
- record_step(state, accepted, parent_energy=None)[source]
Record one chain iteration as a delta from the previous assignment.
- begin_level(level, partition=None)[source]
Begin recording tree cuts for a level. :param level: “supergraph” or “base” :param partition: The current partition (used to compute supergraph node coordinates).
- Parameters:
level (str)
- record_tree_cut(tree_edges, root, cut_node, psi_chosen, psi_total, n_cuts, spanning_tree_obj=None, extracted_nodes=None)[source]
Record one Phase 3: Capacitated Tree Cut.
- end_level(centers=None)[source]
Finish recording a level. Packs tree cuts into Phase 2 + Phase 4. :param centers: Dict of facility centers for this level.
- record_select(supergraph, selected_superdistricts, merged_base_nodes, partition=None)[source]
Record the superdistrict selection between upper and lower levels.
- record_accept_reject(proposed_state, current_state, accepted)[source]
Record the accept/reject decision.
- record_substep(tree_edges, root, cut_node, psi_chosen, psi_total, n_cuts, spanning_tree_obj=None)[source]
Delegates to record_tree_cut.
falcomchain.markovchain
- class falcomchain.markovchain.MarkovChain(proposal, constraints, accept, initial_state, total_steps, recorder=None, callbacks=None)[source]
MarkovChain is a class that creates an iterator for iterating over the states of a Markov chain run in a gerrymandering analysis context.
It allows for the generation of a sequence of partitions (states) of a political districting plan, where each partition represents a possible state in the Markov chain.
Example usage:
chain = MarkovChain(proposal, constraints, accept, initial_state, total_steps) for state in chain: # Do whatever you want - print output, compute scores, ...
- Parameters:
- class falcomchain.markovchain.state.ChainState(partition, facility, energy, log_proposal_ratio, beta, feasible, super_facility=None, energy_fn=None, super_facility_fn=None)[source]
Wraps a Partition with the MCMC context needed for the Metropolis-Hastings acceptance step.
The proposal step is responsible for setting energy, log_proposal_ratio, and feasible before returning a ChainState. beta is set once at chain initialisation and copied forward.
- Variables:
partition – The underlying partition state.
energy – E(s) — access energy of this state.
log_proposal_ratio –
log( q(s|s') / q(s'|s) )from the proposal.beta – Inverse temperature (>= 0).
feasible – R(s’) — hard constraint indicator.
- Parameters:
partition (Partition)
facility (FacilityAssignment)
energy (float)
log_proposal_ratio (float)
beta (float)
feasible (bool)
super_facility (SuperFacilityAssignment | None)
- classmethod initial(partition, energy, beta, energy_fn=None, super_facility_fn=None)[source]
Construct the initial ChainState at the start of the chain. The proposal ratio is 0 (log 1) and the state is always feasible. FacilityAssignment is computed eagerly for all districts.
- Parameters:
partition (Partition) – The initial partition.
energy (float) – E(s_0) of the initial partition.
beta (float) – Inverse temperature.
energy_fn – Optional custom energy function(state) -> float. Defaults to
compute_energy(demand-weighted travel time).super_facility_fn – Optional callable
(state) -> SuperFacilityAssignmentthat produces the level-2 facility assignment.None(default) means level-2 facilities are not computed;state.super_facilityisNonefor the entire chain. PassSuperFacilityAssignment.from_stateto enable the default Eq. 18 minimax selector, or any custom callable.
- Return type:
- class falcomchain.markovchain.facility.FacilityAssignment(travel_times, demands=None)[source]
Computes and caches the best facility center and access cost for each district, given a ChainState.
The optimal center is the demand-weighted 1-median: the candidate that minimises the total demand-weighted travel time
cost(D, f) = sum_{v in D} d_v * dist(f, v)
from the facility to every node in the district. This matches the base-level term of the disaggregated hierarchical median objective (paper Eq. for the MILP benchmark) and is the optimization analogue that the chain’s facility-assignment rule (paper Section 5.4) targets.
Centers and costs are computed eagerly and cached. Only districts that changed (via
flow.node_flowsandflow.part_flows) are recomputed inupdated().- Variables:
centers – Maps district ID → demand-weighted 1-median node.
radii – Maps district ID → 1-median access cost
sum_v d_v * dist(center, v)(NOT a covering radius). The name is retained for backward compatibility of accessors.
- Parameters:
- classmethod from_state(state)[source]
Build a fully computed FacilityAssignment from a ChainState. All districts are evaluated eagerly.
- Parameters:
state – A ChainState whose assignment has
travel_timesset.- Return type:
- classmethod updated(previous, state)[source]
Build a FacilityAssignment for
stateby copying the previous one and recomputing only the districts that changed.- Parameters:
previous (FacilityAssignment) – FacilityAssignment from the parent ChainState.
state – The proposed ChainState.
- Return type:
- class falcomchain.markovchain.facility.SuperFacilityAssignment[source]
Computes level-2 (super-) facility centers for each superdistrict.
For each superdistrict D^2, the level-2 facility is selected from F^2 ∩ V(G^1[D^2]) — the super-candidates (
super_candidate=1nodes) that lie in the base-level subgraph induced by D^2’s constituent level-1 districts. Selection is delegated to a pluggable callable (seeselection_fn); the default isminimax_super_selector()(Eq. 18).Soft constraint at level 2. If a superdistrict contains no super-candidates, no level-2 facility is assigned for it — the entry is simply absent from
centersandradii. The chain is not rejected. Callers can iteratestate.partition.super_partsto detect superdistricts without a level-2 facility.- Variables:
centers – Maps superdistrict ID -> selected super-candidate node. May not contain an entry for every superdistrict (soft constraint).
radii – Maps superdistrict ID -> covering radius (matches the keys of
centers).
- classmethod from_state(state, selection_fn=None)[source]
Build a SuperFacilityAssignment from a ChainState.
Iterates
state.partition.super_parts(superdistrict ID -> set of level-1 district IDs), gathers super-candidates and base nodes for each, and delegates the actual selection toselection_fn.- Parameters:
state – A ChainState whose partition has
super_assignmentpopulated and whoseassignment.travel_timesis set. The level-1 facility centers instate.facilitymust already be populated (they are the “spokes” the L2 facility serves).selection_fn – Callable
(super_id, super_candidates, base_nodes, demands, travel_times) -> (node, cost). Defaults tominimax_super_selector()(demand-weighted 1-median over base nodes).
- Return type:
- classmethod updated(previous, state, selection_fn=None)[source]
Build a SuperFacilityAssignment incrementally from
previous.Mirrors
FacilityAssignment.updated()at level 2: copiesprevious’scentersandradiiand recomputes only the super-districts that changed this step. Unchanged super-districts keep their cached center / radius.Cheaper than
from_state()when the partition’ssuperfliponly touches a few super-districts (which is the common case underhierarchical_recom— exactly one super-district is merged and re-partitioned per step).- Parameters:
previous (SuperFacilityAssignment) – The parent state’s
SuperFacilityAssignment.state – The proposed
ChainState.selection_fn – See
from_state().
- Return type:
- falcomchain.markovchain.proposals.hierarchical_recom(state, epsilon_base, epsilon_super, demand_target, density=None, super_partitioner=<function resample_super_partition>, gamma_base=0.0, gamma_super=0.0, psi_fn=None, super_psi_fn=None, travel_times=None, c_min_base=1, c_min_super=None, c_max_super=None, min_districts_super=1, max_attempts_super=1000, rule='per_team', enforce_global_balance=False)[source]
Proposes a new ChainState via two-level hierarchical ReCom.
The upper-level (supergraph) partitioning is delegated to
super_partitioner. The defaultresample_super_partition()samples a fresh level-2 partition each step. Passfixed_super_partition()(or any custom callable with the same signature) for alternative behavior such as fixed superdistricts.Once the upper level returns the chosen superdistrict’s level-1 IDs, those base nodes are re-partitioned via recursive partitioning to produce the new level-1 districts. Recording (if enabled) is driven by the
Recorderattached to the chain viastate._recorder.- Parameters:
state – The current ChainState.
epsilon_base (float) – Level-1 demand-balance tolerance ε¹ (paper). Used for the lower-level recursive partitioning of the chosen superdistrict.
epsilon_super (float) – Level-2 demand-balance tolerance ε² (paper). Used by the upper-level
super_partitioner.demand_target (float) – Per-team workload d̄ (paper).
density (float | None) – Optional density parameter.
super_partitioner (Callable) – Callable with signature
(state, epsilon_super, demand_target, density, recorder, super_psi_fn, gamma_super) -> (super_flip, merge, super_teams, super_demand, log_super_ratio). Defaults toresample_super_partition().gamma_super (float) – Inverse temperature γ² for the level-2 hub-coherence score (paper Eq. 22, 27). When
> 0andsuper_psi_fnisNone, a hub-coherence scorer is built fromstate. Default 0.super_psi_fn (Callable | None) – Optional precomputed level-2 scorer
(subnodes, teams) -> float. Overrides the default factory built fromgamma_super.c_min_base (int) – Minimum level-1 capacity per district (default 1).
c_min_super (int | None) – Minimum level-2 capacity (number of level-1 districts per super-district). Defaults to
2 · c_min_base— the standard choice that keeps super-districts non-trivial and aligns the supergraph’s leaf demand range with the actual super-node weights (≈c¹·w).c_max_super (int | None) – Maximum level-2 capacity (paper §6.4 default: 3). Required.
gamma_base (float)
psi_fn (Callable | None)
travel_times (dict | None)
min_districts_super (int)
max_attempts_super (int)
rule (str)
enforce_global_balance (bool)
- Returns:
The proposed ChainState.
- falcomchain.markovchain.accept.always_accept(_proposed, _current)[source]
Acceptance function that accepts every proposed state unconditionally.
This is the paper’s intended acceptance rule (Section 6.3): a proposal is feasible iff every cut has ψ > 0, and feasible proposals are accepted unconditionally. Use this for ensemble sampling.
- Parameters:
proposed – The proposed chain state.
current – The current chain state (unused).
_proposed (ChainState)
_current (ChainState)
- Returns:
Always
True.- Return type:
- falcomchain.markovchain.accept.boltzmann(state, parent)[source]
Boltzmann acceptance rule for energy-biased optimization.
Accepts the proposed state with probability
alpha(s -> s’) = min(1, R(s’) * exp(-beta * (E(s’) - E(s))))
where:
R(s')is the hard feasibility indicator →state.feasibleE(s')is the energy of the proposed state →state.energyE(s)is the energy of the current state →parent.energybetais the inverse temperature →state.beta
Requires the user to have explicitly opted into an objective by passing
energy_fn=...toChainState.initial(). Without an energy function FalCom is a sampler — call this only when you’ve decided to optimize.Note
This is not a true Metropolis-Hastings sampler. It omits the proposal-density ratio
q(s|s')/q(s'|s)because that ratio is intractable for FalCom (paper Section 6.3): it depends on the number of admissible cuts across all spanning trees of the merged subgraph in both the current and proposed states, which is #P-hard to compute (Jerrum & Sinclair). As a result this function is a heuristic optimizer that drives the chain toward low-energy states, not a sampler from a Boltzmann distribution. For sampling with convergence guarantees, usealways_accept().- Parameters:
state (ChainState) – The proposed ChainState.
parent (ChainState) – The current ChainState.
- Returns:
True if the proposal is accepted, False otherwise.
- Return type:
- Raises:
RuntimeError – If
state.energy_fnisNone— this would mean Boltzmann is being called without an objective, which would silently degrade toalways_accept. Setenergy_fnonChainState.initial(e.g.energy_fn=falcomchain.markovchain.energy.compute_energy).
- falcomchain.markovchain.energy.compute_energy(state)[source]
Disaggregated hierarchical median energy (the FalCom optimization objective; matches the MILP benchmark objective of the experiments section):
- E(s) = Σ_D Σ_{v∈D} d_v·dist(v, f¹(D)) (base-level access)
Σ_S Σ_{v∈V¹[S]} d_v·dist(v, f²(S)) (upper-level coordination)
where D ranges over level-1 districts with level-1 facility f¹(D), S ranges over superdistricts with level-2 facility f²(S), d_v is node demand, and dist is read from
Assignment.travel_times. The two terms are separable across levels: each facility is the demand-weighted 1-median of the units it serves.The level-2 term is included only when
state.super_facilityis populated (i.e. asuper_facility_fnwas supplied); otherwise the energy is the base-level access term alone.- Parameters:
state (ChainState) – The current chain state.
- Returns:
Disaggregated median (lower is better).
- Return type:
- falcomchain.markovchain.energy.compute_energy_delta(proposed, current)[source]
Compute E(proposed) - E(current) efficiently by re-evaluating only the districts that changed (those in
proposed.partition.flow.node_flows).- Parameters:
proposed (ChainState) – The proposed chain state.
current (ChainState) – The current chain state.
- Returns:
Delta energy E(s’) - E(s).
- Return type:
falcomchain.constraints
- class falcomchain.constraints.Validator(constraints)[source]
A single callable for checking that a partition passes a collection of constraints. Intended to be passed as the
is_validparameter when instantiatingMarkovChain.This class is meant to be called as a function after instantiation; its return is
Trueif all validators pass, andFalseif any one fails.Example usage:
is_valid = Validator([constraint1, constraint2, constraint3]) chain = MarkovChain(proposal, is_valid, accept, initial_state, total_steps)
- Variables:
constraints – List of validator functions that will check partitions.
- Parameters:
constraints (List[Callable])
falcomchain.random
Isolated random number generator for reproducible chain runs.
All stochastic operations in the library use falcomchain.random.rng
instead of the global random module. This ensures that external
library calls (numpy, networkx, etc.) cannot perturb the chain’s
random state.
Usage:
from falcomchain.random import rng, set_seed
set_seed(2025) # deterministic from here
rng.choice([1, 2, 3]) # use rng instead of random
To reproduce a chain: call set_seed(s) before constructing the
initial partition. The same seed + same library versions = identical chain.