PropFlow User Guide
This guide presents PropFlow from the top down so you can understand the high-level architecture before working through concrete APIs. Follow the chain from individual agents, through factor graphs and engines, all the way to full-blown simulator runs and analysis tooling.
Top-Down Architecture
PropFlow can be viewed as a layered pipeline. Each layer builds on the previous one, and you can exit early if you only need part of the stack:
Agents (
propflow.core.agents.VariableAgent,propflow.core.agents.FactorAgent) exchange messages.Factor graphs (
propflow.bp.factor_graph.FactorGraph) connect agents and initialize cost tables. Helper builders live inpropflow.utils.Engines (
propflow.bp.engine_base.BPEngineand subclasses) run belief propagation, manage convergence policies, and capture history.Simulations (
propflow.simulator.Simulator) execute batches of engine configurations across many graphs for fair comparisons.Analysis tooling (
propflow.snapshots) records data and reports metrics for offline inspection.
Agents Layer
Agents are the smallest active components in PropFlow. They inherit from
propflow.core.agents.FGAgent, which embeds an inbox/outbox, message
history, and a link to a computator implementing the BP math.
Variable Agents
propflow.core.agents.VariableAgent models a discrete decision variable.
Important attributes and behaviours:
name– identifier used in logs and assignments.domain– number of discrete values this variable may take.belief– vector computed by the attachedcomputator(defaults to uniform if messages are missing).curr_assignment– best value implied by the current belief.compute_messages()– callscomputator.compute_Qto prepare messages for neighbouring factors.
Example:
from propflow.core import VariableAgent
temperature = VariableAgent(name="temp_room_a", domain=4)
Factor Agents
propflow.core.agents.FactorAgent encodes the local relationships
between several variables. Each factor owns a cost table that is lazily created
from a factory function.
Key fields:
cost_table–numpy.ndarrayscoring each variable assignment tuple.ct_creation_func/ct_creation_params– factory for building the table. The factor graph callsinitiate_cost_table()once the neighbourhood is known.connection_number– mapping of variable names to axis indices. Maintained automatically when you add edges.compute_messages()– usescomputator.compute_Rto send responses back to variables.
from propflow.core import FactorAgent
from propflow.configs import create_random_int_table
penalty = FactorAgent(
name="f_xy",
domain=3,
ct_creation_func=create_random_int_table,
param={"low": 0, "high": 10},
)
Message Lifecycle
Agents exchange propflow.core.components.Message objects stored within
a propflow.core.components.MailHandler. The handler:
Deduplicates messages per sender.
Seeds zero-messages so every neighbour pair can exchange information on the very first engine iteration.
Stages outgoing messages until the engine triggers delivery.
You seldom interact with messages directly unless you’re implementing new BP variants.
Factor Graph Layer
With agents in hand, propflow.bp.factor_graph.FactorGraph wires them
into a bipartite structure, initializes cost tables, and exposes convenience
properties such as the graph diameter and current assignments. Most users
should rely on propflow.utils.FGBuilder to create graphs. The helpers
ensure domain sizes line up, edges are valid, and factors receive their cost
tables automatically.
Using FGBuilder
FGBuilder covers common topologies so you can focus on experiments instead
of plumbing. The snippet below builds a cycle and runs a plain BP engine:
from propflow import FGBuilder, BPEngine
from propflow.configs import create_random_int_table
fg = FGBuilder.build_cycle_graph(
num_vars=5,
domain_size=3,
ct_factory=create_random_int_table,
ct_params={"low": 0, "high": 10},
)
engine = BPEngine(fg)
engine.run(max_iter=25)
print(engine.assignments)
Other helpers such as propflow.utils.fg_utils.FGBuilder.build_random_graph()
return fully initialised FactorGraph objects as well.
Cost-Table Factories
Cost-table factories can be passed as raw callables, registry strings, or members
of propflow.configs.CTFactories.
from propflow.configs import CTFactories, get_ct_factory
random_int = CTFactories.RANDOM_INT
uniform = get_ct_factory("uniform_float")
Built-in registry keys are "random_int", "uniform_float", and
"poisson". Custom factories should accept (num_vars, domain_size,
**kwargs) and return a NumPy array shaped (domain_size,) * num_vars.
Manual Graph Assembly
When you need a structure that the helpers do not cover—custom agents, hybrid
domains—build the graph yourself. Provide explicit lists of variables, factors,
and an ordered edges mapping.
from propflow import FactorGraph, VariableAgent, FactorAgent
from propflow.configs import create_uniform_float_table
x1 = VariableAgent("x1", domain=2)
x2 = VariableAgent("x2", domain=2)
parity = FactorAgent(
name="f12",
domain=2,
ct_creation_func=create_uniform_float_table,
)
fg = FactorGraph(
variable_li=[x1, x2],
factor_li=[parity],
edges={parity: [x1, x2]},
)
Checklist for manual graphs:
Every factor supplied in
factor_liappears as a key inedges.Each value in
edgesis an ordered list; the index order defines tensor axes, so be deliberate when mapping variables to dimensions.ct_creation_funcmust acceptnum_varsanddomain_sizearguments; PropFlow passes them automatically.Use deterministic parameters (bounds, seeds) when you want reproducible runs.
Engine Layer
Engines coordinate message passing, convergence behaviour, history tracking,
and optional snapshots. The base propflow.bp.engine_base.BPEngine
implements synchronous belief propagation: variables update first, then factors,
for each iteration.
Core responsibilities:
Assign the chosen
propflow.core.dcop_base.Computatorto every agent.Seed inboxes with zero-messages so computation can start immediately.
Execute
steploops until convergence or a maximum iteration cap.Record costs, beliefs, assignments, and messages through automatic snapshots exposed via a read-only history compatibility view.
Expose hook methods (
pre_factor_computeetc.) that subclasses override to implement policies.
Selecting a Computator
Computators contain the algorithmic math. PropFlow ships with:
propflow.bp.computators.MinSumComputator(default)
Swap variants by passing the desired instance to the engine:
from propflow import BPEngine, MaxSumComputator
engine = BPEngine(fg, computator=MaxSumComputator())
Engine Variants and Policies
Specialised engines extend BPEngine with additional behaviour:
propflow.bp.engines.DampingEngine– damps variable-to-factor Q messages.propflow.bp.engines.RDampingEngine– damps factor-to-variable R messages.propflow.bp.engines.QRDampingEngine– damps Q and R messages independently.propflow.bp.engines.DiffusionEngine– spatially blends same-neighbourhood messages.propflow.bp.engines.SplitEngine– splits factors at initialization.propflow.bp.engines.MidRunSplitEngine– applies factor splitting during a run.propflow.bp.engines.CostReductionOnceEngine– reduces costs once at startup.propflow.bp.engines.TRWEngine– applies tree-reweighted Min-Sum scaling.propflow.bp.engines.DampingTRWEngine– combines damping with TRW scaling.propflow.bp.engines.MessagePruningEngine– initializes a message-pruning policy.
Complement engines with policies and utilities:
propflow.policies.convergance.ConvergenceConfigto define minimum iterations, tolerance, and patience.propflow.policies.normalize_cost.normalize_inbox()to shift messages and avoid numerical blow-ups.Built-in snapshot capture (
engine.snapshots) to inspect per-step state.
Running a Single Engine
from propflow import BPEngine, MinSumComputator
engine = BPEngine(
factor_graph=fg,
computator=MinSumComputator(),
)
engine.run(max_iter=100)
final_cost = engine.snapshots[-1].global_cost
beliefs = engine.get_beliefs()
Inspect engine.assignments or iterate over engine.snapshots
for detailed per-step data. engine.latest_snapshot() returns the most
recent snapshot object.
Simulation Layer
The propflow.simulator.Simulator orchestrates multiple engine
configurations running over many graphs—perfect for benchmarking or tuning.
Prepare a configuration dictionary mapping experiment names to engine classes plus keyword arguments.
Build a list of factor graphs (reuse
FGBuilderhelpers or load pickled graphs).Call
Simulator.run_simulations()to execute everything. The simulator attempts to run in parallel usingmultiprocessingbut falls back to sequential processing if required.Use
Simulator.plot_results()to visualise mean cost trajectories.
from propflow import Simulator, BPEngine, DampingEngine, FGBuilder
from propflow.configs import create_random_int_table
configs = {
"baseline": {"class": BPEngine},
"damped": {"class": DampingEngine, "damping_factor": 0.85},
}
graphs = [
FGBuilder.build_random_graph(
num_vars=12,
domain_size=3,
ct_factory=create_random_int_table,
ct_params={"low": 0, "high": 15},
density=0.25,
)
for _ in range(4)
]
simulator = Simulator(configs)
aggregated = simulator.run_simulations(graphs, max_iter=150)
simulator.plot_results(verbose=True)
Analysis Layer
Advanced studies often require visibility into per-iteration behaviour. The
propflow.snapshots package provides the core tooling:
propflow.snapshots.SnapshotAnalyzerandpropflow.snapshots.AnalysisReportderive metrics, block norms, and summaries directly fromengine.snapshots.propflow.snapshots.SnapshotVisualizerrenders belief argmin trajectories and message norms.
Example workflow:
import json
from pathlib import Path
from propflow.snapshots import SnapshotAnalyzer, AnalysisReport
from propflow.snapshots import SnapshotVisualizer
engine = BPEngine(fg)
engine.run(max_iter=75)
snapshots = list(engine.snapshots)
out_path = Path("results/run_001.json")
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(json.dumps([
{
"step": snap.step,
"assignments": snap.assignments,
"global_cost": snap.global_cost,
}
for snap in snapshots
], indent=2))
viz = SnapshotVisualizer(snapshots)
viz.plot_argmin_per_variable(show=True)
analyzer = SnapshotAnalyzer(snapshots)
report = AnalysisReport(analyzer)
summary = report.to_json(step_idx=len(snapshots) - 1)
Chain of Creation
Use this checklist when building your own experiments:
Choose a graph strategy
Prefer
propflow.utils.FGBuilderfor standard cycles or random graphs.Fall back to manual agent construction when you need custom structures.
Instantiate the factor graph
Pass lists of variable and factor agents plus an ordered
edgesmap.Confirm domain sizes match the factor expectations.
Pick an engine configuration
Select a
computatorand, if needed, an engine variant with policies.Configure convergence rules to match your evaluation criteria.
Run experiments
Call
BPEngine.run()for single cases.Use
Simulatorto fan out across many graphs/configurations.
Analyse results
Inspect
engine.snapshotsfor per-step assignments, messages, and costs.Use
propflow.snapshotsfor visualisation and metric reporting.
Custom Graph Checklist
If you bypass FGBuilder:
Ensure every
propflow.core.agents.FactorAgentreferences each neighbouringpropflow.core.agents.VariableAgentexactly once.Provide cost-table factories that honour the
(num_vars, domain_size)signature—the FactorGraph constructor will call them for you.Call
FactorGraphonly after all agents exist; it registers edges and triggers cost table creation automatically.Stick to deterministic seeds and bounds inside your cost factories for reproducible results.
Next Steps
Jump to Quick Start Guide for runnable snippets.
Browse Examples for complete demonstrations and notebooks.
Consult API Reference for the full API surface.
Review PropFlow Handbook for deeper dives, patterns, and practices.